@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,40 @@
1
+ import { INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list"
2
+ import { $setBlocksType } from "@lexical/selection"
3
+ import { $createParagraphNode, $getSelection, $isRangeSelection } from "lexical"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+
10
+ const BLOCK_FORMAT_VALUE = "bullet"
11
+
12
+ export function FormatBulletedList() {
13
+ const { activeEditor, blockType } = useToolbarContext()
14
+
15
+ const formatParagraph = () => {
16
+ activeEditor.update(() => {
17
+ const selection = $getSelection()
18
+ if ($isRangeSelection(selection)) {
19
+ $setBlocksType(selection, () => $createParagraphNode())
20
+ }
21
+ })
22
+ }
23
+
24
+ const formatBulletedList = () => {
25
+ if (blockType !== "bullet") {
26
+ activeEditor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
27
+ } else {
28
+ formatParagraph()
29
+ }
30
+ }
31
+
32
+ return (
33
+ <SelectItem value={BLOCK_FORMAT_VALUE} onPointerDown={formatBulletedList}>
34
+ <Flex align="center" gap={2}>
35
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.icon}
36
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.label}
37
+ </Flex>
38
+ </SelectItem>
39
+ )
40
+ }
@@ -0,0 +1,40 @@
1
+ import { INSERT_CHECK_LIST_COMMAND } from "@lexical/list"
2
+ import { $setBlocksType } from "@lexical/selection"
3
+ import { $createParagraphNode, $getSelection, $isRangeSelection } from "lexical"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+
10
+ const BLOCK_FORMAT_VALUE = "check"
11
+
12
+ export function FormatCheckList() {
13
+ const { activeEditor, blockType } = useToolbarContext()
14
+
15
+ const formatParagraph = () => {
16
+ activeEditor.update(() => {
17
+ const selection = $getSelection()
18
+ if ($isRangeSelection(selection)) {
19
+ $setBlocksType(selection, () => $createParagraphNode())
20
+ }
21
+ })
22
+ }
23
+
24
+ const formatCheckList = () => {
25
+ if (blockType !== "check") {
26
+ activeEditor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined)
27
+ } else {
28
+ formatParagraph()
29
+ }
30
+ }
31
+
32
+ return (
33
+ <SelectItem value={BLOCK_FORMAT_VALUE} onPointerDown={formatCheckList}>
34
+ <Flex align="center" gap={2}>
35
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.icon}
36
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.label}
37
+ </Flex>
38
+ </SelectItem>
39
+ )
40
+ }
@@ -0,0 +1,45 @@
1
+ import { $createCodeNode } from "@lexical/code"
2
+ import { $setBlocksType } from "@lexical/selection"
3
+ import { $getSelection, $isRangeSelection } from "lexical"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+
10
+ const BLOCK_FORMAT_VALUE = "code"
11
+
12
+ export function FormatCodeBlock() {
13
+ const { activeEditor, blockType } = useToolbarContext()
14
+
15
+ const formatCode = () => {
16
+ if (blockType !== "code") {
17
+ activeEditor.update(() => {
18
+ let selection = $getSelection()
19
+
20
+ if (selection !== null) {
21
+ if (selection.isCollapsed()) {
22
+ $setBlocksType(selection, () => $createCodeNode())
23
+ } else {
24
+ const textContent = selection.getTextContent()
25
+ const codeNode = $createCodeNode()
26
+ selection.insertNodes([codeNode])
27
+ selection = $getSelection()
28
+ if ($isRangeSelection(selection)) {
29
+ selection.insertRawText(textContent)
30
+ }
31
+ }
32
+ }
33
+ })
34
+ }
35
+ }
36
+
37
+ return (
38
+ <SelectItem value={BLOCK_FORMAT_VALUE} onPointerDown={formatCode}>
39
+ <Flex align="center" gap={2}>
40
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.icon}
41
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.label}
42
+ </Flex>
43
+ </SelectItem>
44
+ )
45
+ }
@@ -0,0 +1,34 @@
1
+ import { $createHeadingNode, HeadingTagType } from "@lexical/rich-text"
2
+ import { $setBlocksType } from "@lexical/selection"
3
+ import { $getSelection } from "lexical"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+
10
+ export function FormatHeading({ levels = [] }: { levels: HeadingTagType[] }) {
11
+ const { activeEditor, blockType } = useToolbarContext()
12
+
13
+ const formatHeading = (headingSize: HeadingTagType) => {
14
+ if (blockType !== headingSize) {
15
+ activeEditor.update(() => {
16
+ const selection = $getSelection()
17
+ $setBlocksType(selection, () => $createHeadingNode(headingSize))
18
+ })
19
+ }
20
+ }
21
+
22
+ return levels.map((level) => (
23
+ <SelectItem
24
+ key={level}
25
+ value={level}
26
+ onPointerDown={() => formatHeading(level)}
27
+ >
28
+ <Flex align="center" gap={2}>
29
+ {blockTypeToBlockName[level]?.icon}
30
+ {blockTypeToBlockName[level]?.label}
31
+ </Flex>
32
+ </SelectItem>
33
+ ))
34
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ INSERT_ORDERED_LIST_COMMAND,
3
+ INSERT_UNORDERED_LIST_COMMAND,
4
+ } from "@lexical/list"
5
+ import { $setBlocksType } from "@lexical/selection"
6
+ import { $getNearestNodeOfType } from "@lexical/utils"
7
+ import {
8
+ $createParagraphNode,
9
+ $getSelection,
10
+ $isRangeSelection,
11
+ } from "lexical"
12
+
13
+ import { useToolbarContext } from "../../../context/toolbar-context"
14
+ import { ListWithColorNode } from "../../../nodes/list-with-color-node"
15
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
16
+ import { Flex } from "../../../ui/flex"
17
+ import { SelectItem } from "../../../ui/select"
18
+
19
+ interface FormatListWithMarkerProps {
20
+ blockFormatValue: string
21
+ listType: "bullet" | "number"
22
+ markerType: string
23
+ }
24
+
25
+ export function FormatListWithMarker({
26
+ blockFormatValue,
27
+ listType,
28
+ markerType,
29
+ }: FormatListWithMarkerProps) {
30
+ const { activeEditor, blockType } = useToolbarContext()
31
+
32
+ const formatParagraph = () => {
33
+ activeEditor.update(() => {
34
+ const selection = $getSelection()
35
+ if ($isRangeSelection(selection)) {
36
+ $setBlocksType(selection, () => $createParagraphNode())
37
+ }
38
+ })
39
+ }
40
+
41
+ const formatList = () => {
42
+ if (blockType !== blockFormatValue) {
43
+ const command =
44
+ listType === "bullet"
45
+ ? INSERT_UNORDERED_LIST_COMMAND
46
+ : INSERT_ORDERED_LIST_COMMAND
47
+
48
+ activeEditor.dispatchCommand(command, undefined)
49
+
50
+ // Update marker type immediately after
51
+ activeEditor.update(() => {
52
+ const selection = $getSelection()
53
+ if ($isRangeSelection(selection)) {
54
+ const anchorNode = selection.anchor.getNode()
55
+ const listNode = $getNearestNodeOfType(anchorNode, ListWithColorNode)
56
+ if (listNode) {
57
+ listNode.setMarkerType(markerType)
58
+ }
59
+ }
60
+ })
61
+ } else {
62
+ formatParagraph()
63
+ }
64
+ }
65
+
66
+ return (
67
+ <SelectItem value={blockFormatValue} onPointerDown={formatList}>
68
+ <Flex align="center" gap={2}>
69
+ {blockTypeToBlockName[blockFormatValue]?.icon}
70
+ {blockTypeToBlockName[blockFormatValue]?.label}
71
+ </Flex>
72
+ </SelectItem>
73
+ )
74
+ }
@@ -0,0 +1,40 @@
1
+ import { INSERT_ORDERED_LIST_COMMAND } from "@lexical/list"
2
+ import { $setBlocksType } from "@lexical/selection"
3
+ import { $createParagraphNode, $getSelection, $isRangeSelection } from "lexical"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+
10
+ const BLOCK_FORMAT_VALUE = "number"
11
+
12
+ export function FormatNumberedList() {
13
+ const { activeEditor, blockType } = useToolbarContext()
14
+
15
+ const formatParagraph = () => {
16
+ activeEditor.update(() => {
17
+ const selection = $getSelection()
18
+ if ($isRangeSelection(selection)) {
19
+ $setBlocksType(selection, () => $createParagraphNode())
20
+ }
21
+ })
22
+ }
23
+
24
+ const formatNumberedList = () => {
25
+ if (blockType !== "number") {
26
+ activeEditor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)
27
+ } else {
28
+ formatParagraph()
29
+ }
30
+ }
31
+
32
+ return (
33
+ <SelectItem value={BLOCK_FORMAT_VALUE} onPointerDown={formatNumberedList}>
34
+ <Flex align="center" gap={2}>
35
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.icon}
36
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.label}
37
+ </Flex>
38
+ </SelectItem>
39
+ )
40
+ }
@@ -0,0 +1,31 @@
1
+ import { $setBlocksType } from "@lexical/selection"
2
+ import { $createParagraphNode, $getSelection, $isRangeSelection } from "lexical"
3
+
4
+ import { useToolbarContext } from "../../../context/toolbar-context"
5
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
6
+ import { SelectItem } from "../../../ui/select"
7
+ import { Flex } from "../../../ui/flex"
8
+
9
+ const BLOCK_FORMAT_VALUE = "paragraph"
10
+
11
+ export function FormatParagraph() {
12
+ const { activeEditor } = useToolbarContext()
13
+
14
+ const formatParagraph = () => {
15
+ activeEditor.update(() => {
16
+ const selection = $getSelection()
17
+ if ($isRangeSelection(selection)) {
18
+ $setBlocksType(selection, () => $createParagraphNode())
19
+ }
20
+ })
21
+ }
22
+
23
+ return (
24
+ <SelectItem value={BLOCK_FORMAT_VALUE} onPointerDown={formatParagraph}>
25
+ <Flex align="center" gap={2}>
26
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.icon}
27
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.label}
28
+ </Flex>
29
+ </SelectItem>
30
+ )
31
+ }
@@ -0,0 +1,32 @@
1
+ import { $createQuoteNode } from "@lexical/rich-text"
2
+ import { $setBlocksType } from "@lexical/selection"
3
+ import { $getSelection } from "lexical"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { blockTypeToBlockName } from "../../../plugins/toolbar/block-format/block-format-data"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+
10
+ const BLOCK_FORMAT_VALUE = "quote"
11
+
12
+ export function FormatQuote() {
13
+ const { activeEditor, blockType } = useToolbarContext()
14
+
15
+ const formatQuote = () => {
16
+ if (blockType !== "quote") {
17
+ activeEditor.update(() => {
18
+ const selection = $getSelection()
19
+ $setBlocksType(selection, () => $createQuoteNode())
20
+ })
21
+ }
22
+ }
23
+
24
+ return (
25
+ <SelectItem value={BLOCK_FORMAT_VALUE} onPointerDown={formatQuote}>
26
+ <Flex align="center" gap={2}>
27
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.icon}
28
+ {blockTypeToBlockName[BLOCK_FORMAT_VALUE]?.label}
29
+ </Flex>
30
+ </SelectItem>
31
+ )
32
+ }
@@ -0,0 +1,117 @@
1
+ "use client"
2
+
3
+ import { $isListNode, ListNode } from "@lexical/list"
4
+ import { $isHeadingNode } from "@lexical/rich-text"
5
+ import { $findMatchingParent, $getNearestNodeOfType } from "@lexical/utils"
6
+ import {
7
+ $isRangeSelection,
8
+ $isRootOrShadowRoot,
9
+ BaseSelection,
10
+ } from "lexical"
11
+ import { useCallback } from "react"
12
+
13
+ import { useToolbarContext } from "../../context/toolbar-context"
14
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
15
+ import { ListWithColorNode } from "../../nodes/list-with-color-node"
16
+ import { blockTypeToBlockName } from "../../plugins/toolbar/block-format/block-format-data"
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectGroup,
21
+ SelectTrigger,
22
+ } from "../../ui/select"
23
+
24
+ export function BlockFormatDropDown({
25
+ children,
26
+ }: {
27
+ children: React.ReactNode
28
+ }) {
29
+ const { activeEditor, blockType, setBlockType } = useToolbarContext()
30
+ const currentBlockType = blockTypeToBlockName[blockType]
31
+ ? blockType
32
+ : "paragraph"
33
+ const currentBlockMeta = blockTypeToBlockName[currentBlockType]
34
+
35
+ /**
36
+ * Updates the toolbar state based on the current selection.
37
+ * Identifies if the cursor is in a List, Heading, or other block type.
38
+ */
39
+ const $updateToolbar = useCallback(
40
+ (selection: BaseSelection) => {
41
+ if (!$isRangeSelection(selection)) return
42
+
43
+ const anchorNode = selection.anchor.getNode()
44
+
45
+ // Find the top-level element or the nearest parent that is a root/shadow root
46
+ let element =
47
+ anchorNode.getKey() === "root"
48
+ ? anchorNode
49
+ : $findMatchingParent(anchorNode, (e) => {
50
+ const parent = e.getParent()
51
+ return parent !== null && $isRootOrShadowRoot(parent)
52
+ })
53
+
54
+ if (element === null) {
55
+ element = anchorNode.getTopLevelElementOrThrow()
56
+ }
57
+
58
+ const elementKey = element.getKey()
59
+ const elementDOM = activeEditor.getElementByKey(elementKey)
60
+
61
+ if (elementDOM !== null) {
62
+ if ($isListNode(element)) {
63
+ const parentList = $getNearestNodeOfType<ListNode>(
64
+ anchorNode,
65
+ ListNode
66
+ )
67
+ const listNode = parentList || element
68
+ let type: string = listNode.getListType()
69
+
70
+ if (listNode instanceof ListWithColorNode) {
71
+ const markerType = listNode.getMarkerType()
72
+
73
+ if (type === "bullet") {
74
+ if (markerType === "-") type = "bullet-dash"
75
+ else if (markerType === "+") type = "bullet-plus"
76
+ } else if (type === "number") {
77
+ if (markerType === "alpha") type = "number-alpha"
78
+ }
79
+ }
80
+ setBlockType(type)
81
+ } else {
82
+ const type = $isHeadingNode(element)
83
+ ? element.getTag()
84
+ : element.getType()
85
+ if (type in blockTypeToBlockName) {
86
+ setBlockType(type as keyof typeof blockTypeToBlockName)
87
+ }
88
+ }
89
+ }
90
+ },
91
+ [activeEditor, setBlockType]
92
+ )
93
+
94
+ useUpdateToolbarHandler($updateToolbar)
95
+
96
+ return (
97
+ <Select
98
+ modal={false}
99
+ value={currentBlockType}
100
+ onValueChange={(value) => {
101
+ if (value in blockTypeToBlockName) {
102
+ setBlockType(value as keyof typeof blockTypeToBlockName)
103
+ }
104
+ }}
105
+ >
106
+ <SelectTrigger className="editor-toolbar-select-trigger editor-toolbar-select-trigger--w-md">
107
+ <div className="editor-toolbar-select-icon">
108
+ {currentBlockMeta?.icon}
109
+ </div>
110
+ <span className="editor-truncate editor-block-format-label">{currentBlockMeta?.label}</span>
111
+ </SelectTrigger>
112
+ <SelectContent>
113
+ <SelectGroup>{children}</SelectGroup>
114
+ </SelectContent>
115
+ </Select>
116
+ )
117
+ }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import { Columns3Icon } from "lucide-react"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { InsertLayoutDialog } from "../../../plugins/layout-plugin"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+ import { IconSize } from "../../../ui/typography"
10
+
11
+ export function InsertColumnsLayout() {
12
+ const { activeEditor, showModal } = useToolbarContext()
13
+
14
+ return (
15
+ <SelectItem
16
+ value="columns"
17
+ onPointerUp={() =>
18
+ showModal("Insert Columns Layout", (onClose) => (
19
+ <InsertLayoutDialog activeEditor={activeEditor} onClose={onClose} />
20
+ ))
21
+ }
22
+ className=""
23
+ >
24
+ <Flex align="center" gap={2}>
25
+ <IconSize size="sm">
26
+ <Columns3Icon />
27
+ </IconSize>
28
+ <span>Columns Layout</span>
29
+ </Flex>
30
+ </SelectItem>
31
+ )
32
+ }
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import { useToolbarContext } from "../../../context/toolbar-context"
4
+ import { AutoEmbedDialogStandalone, EmbedConfigs } from "../../../plugins/embeds/auto-embed-plugin"
5
+ import { SelectItem } from "../../../ui/select"
6
+ import { Flex } from "../../../ui/flex"
7
+
8
+ export function InsertEmbeds() {
9
+ const { activeEditor, showModal } = useToolbarContext()
10
+ return EmbedConfigs.map((embedConfig) => (
11
+ <SelectItem
12
+ key={embedConfig.type}
13
+ value={embedConfig.type}
14
+ onPointerUp={() => {
15
+ showModal(`Embed ${embedConfig.contentName}`, (onClose) => (
16
+ <AutoEmbedDialogStandalone
17
+ embedConfig={embedConfig}
18
+ onClose={onClose}
19
+ editor={activeEditor}
20
+ />
21
+ ))
22
+ }}
23
+ className=""
24
+ >
25
+ <Flex align="center" gap={2}>
26
+ {embedConfig.icon}
27
+ <span>{embedConfig.contentName}</span>
28
+ </Flex>
29
+ </SelectItem>
30
+ ))
31
+ }
@@ -0,0 +1,30 @@
1
+ "use client"
2
+
3
+ import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/react/LexicalHorizontalRuleNode"
4
+ import { ScissorsIcon } from "lucide-react"
5
+
6
+ import { useToolbarContext } from "../../../context/toolbar-context"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+ import { IconSize } from "../../../ui/typography"
10
+
11
+ export function InsertHorizontalRule() {
12
+ const { activeEditor } = useToolbarContext()
13
+
14
+ return (
15
+ <SelectItem
16
+ value="horizontal-rule"
17
+ onPointerUp={() =>
18
+ activeEditor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined)
19
+ }
20
+ className=""
21
+ >
22
+ <Flex align="center" gap={2}>
23
+ <IconSize size="sm">
24
+ <ScissorsIcon />
25
+ </IconSize>
26
+ <span>Horizontal Rule</span>
27
+ </Flex>
28
+ </SelectItem>
29
+ )
30
+ }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import { ImageIcon } from "lucide-react"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { InsertImageDialog } from "../../../plugins/images-plugin"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+ import { IconSize } from "../../../ui/typography"
10
+
11
+ export function InsertImage() {
12
+ const { activeEditor, showModal } = useToolbarContext()
13
+
14
+ return (
15
+ <SelectItem
16
+ value="image"
17
+ onPointerUp={() => {
18
+ showModal("Insert Image", (onClose) => (
19
+ <InsertImageDialog activeEditor={activeEditor} onClose={onClose} />
20
+ ))
21
+ }}
22
+ className=""
23
+ >
24
+ <Flex align="center" gap={2}>
25
+ <IconSize size="sm">
26
+ <ImageIcon />
27
+ </IconSize>
28
+ <span>Image</span>
29
+ </Flex>
30
+ </SelectItem>
31
+ )
32
+ }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import { TableIcon } from "lucide-react"
4
+
5
+ import { useToolbarContext } from "../../../context/toolbar-context"
6
+ import { InsertTableDialog } from "../../../plugins/table-plugin"
7
+ import { SelectItem } from "../../../ui/select"
8
+ import { Flex } from "../../../ui/flex"
9
+ import { IconSize } from "../../../ui/typography"
10
+
11
+ export function InsertTable() {
12
+ const { activeEditor, showModal } = useToolbarContext()
13
+
14
+ return (
15
+ <SelectItem
16
+ value="table"
17
+ onPointerUp={() =>
18
+ showModal("Insert Table", (onClose) => (
19
+ <InsertTableDialog activeEditor={activeEditor} onClose={onClose} />
20
+ ))
21
+ }
22
+ className=""
23
+ >
24
+ <Flex align="center" gap={2}>
25
+ <IconSize size="sm">
26
+ <TableIcon />
27
+ </IconSize>
28
+ <span>Table</span>
29
+ </Flex>
30
+ </SelectItem>
31
+ )
32
+ }
@@ -0,0 +1,30 @@
1
+ "use client"
2
+
3
+ import { PlusIcon } from "lucide-react"
4
+
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectGroup,
9
+ SelectTrigger,
10
+ } from "../../ui/select"
11
+ import { IconSize } from "../../ui/typography"
12
+
13
+ export function BlockInsertPlugin({ children }: { children: React.ReactNode }) {
14
+ return (
15
+ <Select
16
+ modal={false}
17
+ value={""}
18
+ >
19
+ <SelectTrigger className="editor-toolbar-item editor-toolbar-item--w-auto editor-toolbar-item--gap-sm">
20
+ <IconSize size="sm">
21
+ <PlusIcon />
22
+ </IconSize>
23
+ <span>Insert</span>
24
+ </SelectTrigger>
25
+ <SelectContent>
26
+ <SelectGroup>{children}</SelectGroup>
27
+ </SelectContent>
28
+ </Select>
29
+ )
30
+ }