@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,101 @@
1
+ "use client"
2
+
3
+ import { useCallback, useState } from "react"
4
+ import {
5
+ $getSelectionStyleValueForProperty,
6
+ $patchStyleText,
7
+ } from "@lexical/selection"
8
+ import { $getSelection, $isRangeSelection, BaseSelection } from "lexical"
9
+ import { BaselineIcon } from "lucide-react"
10
+
11
+ import { useToolbarContext } from "../../context/toolbar-context"
12
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
13
+ import {
14
+ ColorPicker,
15
+ ColorPickerAlphaSlider,
16
+ ColorPickerArea,
17
+ ColorPickerContent,
18
+ ColorPickerEyeDropper,
19
+ ColorPickerFormatSelect,
20
+ ColorPickerHueSlider,
21
+ ColorPickerInput,
22
+ ColorPickerPresets,
23
+ ColorPickerTrigger,
24
+ } from "../../editor-ui/color-picker"
25
+ import { Button } from "../../ui/button"
26
+ import { Flex } from "../../ui/flex"
27
+ import { IconSize } from "../../ui/typography"
28
+
29
+ export function FontColorToolbarPlugin() {
30
+ const { activeEditor } = useToolbarContext()
31
+
32
+ const [fontColor, setFontColor] = useState("#000")
33
+
34
+ const $updateToolbar = (selection: BaseSelection) => {
35
+ if ($isRangeSelection(selection)) {
36
+ setFontColor(
37
+ $getSelectionStyleValueForProperty(selection, "color", "#000")
38
+ )
39
+ }
40
+ }
41
+
42
+ useUpdateToolbarHandler($updateToolbar)
43
+
44
+ const applyStyleText = useCallback(
45
+ (styles: Record<string, string>) => {
46
+ activeEditor.update(() => {
47
+ const selection = $getSelection()
48
+ activeEditor.setEditable(false)
49
+ if (selection !== null) {
50
+ $patchStyleText(selection, styles)
51
+ }
52
+ })
53
+ },
54
+ [activeEditor]
55
+ )
56
+
57
+ const onFontColorSelect = useCallback(
58
+ (value: string) => {
59
+ applyStyleText({ color: value })
60
+ },
61
+ [applyStyleText]
62
+ )
63
+
64
+ return (
65
+ <ColorPicker
66
+ modal
67
+ defaultFormat="hex"
68
+ defaultValue={fontColor}
69
+ onValueChange={onFontColorSelect}
70
+ onOpenChange={(open) => {
71
+ if (!open) {
72
+ activeEditor.setEditable(true)
73
+ activeEditor.focus()
74
+ }
75
+ }}
76
+ >
77
+ <ColorPickerTrigger asChild>
78
+ <Button variant="outline" size="icon" className="editor-toolbar-item--lg">
79
+ <IconSize size="sm">
80
+ <BaselineIcon />
81
+ </IconSize>
82
+ </Button>
83
+ </ColorPickerTrigger>
84
+ <ColorPickerContent>
85
+ <ColorPickerArea />
86
+ <Flex align="center" gap={2}>
87
+ <ColorPickerEyeDropper />
88
+ <Flex direction="column" gap={2} className="editor-flex-1">
89
+ <ColorPickerHueSlider />
90
+ <ColorPickerAlphaSlider />
91
+ </Flex>
92
+ </Flex>
93
+ <Flex align="center" gap={2}>
94
+ <ColorPickerFormatSelect />
95
+ <ColorPickerInput />
96
+ </Flex>
97
+ <ColorPickerPresets />
98
+ </ColorPickerContent>
99
+ </ColorPicker>
100
+ )
101
+ }
@@ -0,0 +1,91 @@
1
+ "use client"
2
+
3
+ import { useCallback, useState } from "react"
4
+ import {
5
+ $getSelectionStyleValueForProperty,
6
+ $patchStyleText,
7
+ } from "@lexical/selection"
8
+ import { $getSelection, $isRangeSelection, BaseSelection } from "lexical"
9
+ import { TypeIcon } from "lucide-react"
10
+
11
+ import { useToolbarContext } from "../../context/toolbar-context"
12
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ } from "../../ui/select"
19
+ import { IconSize } from "../../ui/typography"
20
+
21
+ const FONT_FAMILY_OPTIONS = [
22
+ "Arial",
23
+ "Verdana",
24
+ "Times New Roman",
25
+ "Georgia",
26
+ "Courier New",
27
+ "Trebuchet MS",
28
+ ]
29
+
30
+ export function FontFamilyToolbarPlugin() {
31
+ const style = "font-family"
32
+ const [fontFamily, setFontFamily] = useState("Arial")
33
+
34
+ const { activeEditor } = useToolbarContext()
35
+
36
+ const $updateToolbar = (selection: BaseSelection) => {
37
+ if ($isRangeSelection(selection)) {
38
+ setFontFamily(
39
+ $getSelectionStyleValueForProperty(selection, "font-family", "Arial")
40
+ )
41
+ }
42
+ }
43
+
44
+ useUpdateToolbarHandler($updateToolbar)
45
+
46
+ const handleClick = useCallback(
47
+ (option: string) => {
48
+ activeEditor.update(() => {
49
+ const selection = $getSelection()
50
+ if (selection !== null) {
51
+ $patchStyleText(selection, {
52
+ [style]: option,
53
+ })
54
+ }
55
+ })
56
+ },
57
+ [activeEditor, style]
58
+ )
59
+
60
+ const buttonAriaLabel = "Formatting options for font family"
61
+
62
+ return (
63
+ <Select
64
+ modal={false}
65
+ value={fontFamily}
66
+ onValueChange={(value) => {
67
+ setFontFamily(value)
68
+ handleClick(value)
69
+ }}
70
+ aria-label={buttonAriaLabel}
71
+ >
72
+ <SelectTrigger className="editor-toolbar-select-trigger editor-toolbar-select-trigger--w-auto editor-toolbar-item--gap-sm">
73
+ <IconSize size="sm">
74
+ <TypeIcon />
75
+ </IconSize>
76
+ <span style={{ fontFamily }}>{fontFamily}</span>
77
+ </SelectTrigger>
78
+ <SelectContent>
79
+ {FONT_FAMILY_OPTIONS.map((option) => (
80
+ <SelectItem
81
+ key={option}
82
+ value={option}
83
+ style={{ fontFamily: option }}
84
+ >
85
+ {option}
86
+ </SelectItem>
87
+ ))}
88
+ </SelectContent>
89
+ </Select>
90
+ )
91
+ }
@@ -0,0 +1,85 @@
1
+ "use client"
2
+
3
+ import { useCallback, useState } from "react"
4
+ import { $isTableSelection } from "@lexical/table"
5
+ import {
6
+ $isRangeSelection,
7
+ BaseSelection,
8
+ FORMAT_TEXT_COMMAND,
9
+ TextFormatType,
10
+ } from "lexical"
11
+ import {
12
+ BoldIcon,
13
+ ItalicIcon,
14
+ StrikethroughIcon,
15
+ UnderlineIcon,
16
+ } from "lucide-react"
17
+
18
+ import { useToolbarContext } from "../../context/toolbar-context"
19
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
20
+ import {
21
+ ToggleGroup,
22
+ ToggleGroupItem,
23
+ } from "../../ui/toggle-group"
24
+
25
+ const FORMATS = [
26
+ { format: "bold", icon: BoldIcon, label: "Bold" },
27
+ { format: "italic", icon: ItalicIcon, label: "Italic" },
28
+ { format: "underline", icon: UnderlineIcon, label: "Underline" },
29
+ { format: "strikethrough", icon: StrikethroughIcon, label: "Strikethrough" },
30
+ ] as const
31
+
32
+ export function FontFormatToolbarPlugin() {
33
+ const { activeEditor } = useToolbarContext()
34
+ const [activeFormats, setActiveFormats] = useState<string[]>([])
35
+
36
+ const $updateToolbar = useCallback((selection: BaseSelection) => {
37
+ if ($isRangeSelection(selection) || $isTableSelection(selection)) {
38
+ const formats: string[] = []
39
+ FORMATS.forEach(({ format }) => {
40
+ if (selection.hasFormat(format as TextFormatType)) {
41
+ formats.push(format)
42
+ }
43
+ })
44
+ setActiveFormats((prev) => {
45
+ // Only update if formats have changed
46
+ if (
47
+ prev.length !== formats.length ||
48
+ !formats.every((f) => prev.includes(f))
49
+ ) {
50
+ return formats
51
+ }
52
+ return prev
53
+ })
54
+ }
55
+ }, [])
56
+
57
+ useUpdateToolbarHandler($updateToolbar)
58
+
59
+ return (
60
+ <ToggleGroup
61
+ type="multiple"
62
+ value={activeFormats}
63
+ onValueChange={setActiveFormats}
64
+ variant="default"
65
+ size="sm"
66
+ >
67
+ {FORMATS.map(({ format, icon: Icon, label }) => (
68
+ <ToggleGroupItem
69
+ key={format}
70
+ value={format}
71
+ aria-label={label}
72
+ className="editor-toolbar-item"
73
+ onClick={() => {
74
+ activeEditor.dispatchCommand(
75
+ FORMAT_TEXT_COMMAND,
76
+ format as TextFormatType
77
+ )
78
+ }}
79
+ >
80
+ <Icon className="editor-icon-sm" />
81
+ </ToggleGroupItem>
82
+ ))}
83
+ </ToggleGroup>
84
+ )
85
+ }
@@ -0,0 +1,177 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import {
5
+ $getSelectionStyleValueForProperty,
6
+ $patchStyleText,
7
+ } from "@lexical/selection"
8
+ import { $getSelection, $isRangeSelection, BaseSelection } from "lexical"
9
+ import { Minus, Plus } from "lucide-react"
10
+
11
+ import { useToolbarContext } from "../../context/toolbar-context"
12
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
13
+ import { Button } from "../../ui/button"
14
+ import { ButtonGroup } from "../../ui/button-group"
15
+ import { Input } from "../../ui/input"
16
+ import { IconSize } from "../../ui/typography"
17
+
18
+ const DEFAULT_FONT_SIZE = 16
19
+ const MIN_FONT_SIZE = 1
20
+ const MAX_FONT_SIZE = 72
21
+ const HOLD_DELAY = 500
22
+ const HOLD_INTERVAL = 50
23
+
24
+ export function FontSizeToolbarPlugin() {
25
+ const style = "font-size"
26
+ const [fontSize, setFontSize] = useState<number | string>(DEFAULT_FONT_SIZE)
27
+ const fontSizeRef = useRef(fontSize)
28
+
29
+ // Update ref when state changes
30
+ useEffect(() => {
31
+ fontSizeRef.current = fontSize
32
+ }, [fontSize])
33
+
34
+ const { activeEditor } = useToolbarContext()
35
+
36
+ const $updateToolbar = (selection: BaseSelection) => {
37
+ if ($isRangeSelection(selection)) {
38
+ const value = $getSelectionStyleValueForProperty(
39
+ selection,
40
+ "font-size",
41
+ `${DEFAULT_FONT_SIZE}px`
42
+ )
43
+ const size = parseInt(value) || DEFAULT_FONT_SIZE
44
+ setFontSize(size)
45
+ }
46
+ }
47
+
48
+ useUpdateToolbarHandler($updateToolbar)
49
+
50
+ const updateFontSize = useCallback(
51
+ (newSize: number) => {
52
+ const size = Math.min(Math.max(newSize, MIN_FONT_SIZE), MAX_FONT_SIZE)
53
+ activeEditor.update(() => {
54
+ const selection = $getSelection()
55
+ if (selection !== null) {
56
+ $patchStyleText(selection, {
57
+ [style]: `${size}px`,
58
+ })
59
+ }
60
+ })
61
+ setFontSize(size)
62
+ },
63
+ [activeEditor, style]
64
+ )
65
+
66
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
67
+ const value = e.target.value
68
+ if (value === "") {
69
+ setFontSize("")
70
+ return
71
+ }
72
+ const size = parseInt(value)
73
+ if (!isNaN(size)) {
74
+ setFontSize(size)
75
+ }
76
+ }
77
+
78
+ const handleInputBlur = () => {
79
+ if (fontSize === "" || (typeof fontSize === "number" && (fontSize < MIN_FONT_SIZE || fontSize > MAX_FONT_SIZE))) {
80
+ setFontSize(DEFAULT_FONT_SIZE)
81
+ updateFontSize(DEFAULT_FONT_SIZE)
82
+ } else {
83
+ updateFontSize(fontSize as number)
84
+ }
85
+ }
86
+
87
+ const timerRef = useRef<NodeJS.Timeout | null>(null)
88
+ const intervalRef = useRef<NodeJS.Timeout | null>(null)
89
+
90
+ const stopTimer = useCallback(() => {
91
+ if (timerRef.current) {
92
+ clearTimeout(timerRef.current)
93
+ timerRef.current = null
94
+ }
95
+ if (intervalRef.current) {
96
+ clearInterval(intervalRef.current)
97
+ intervalRef.current = null
98
+ }
99
+ }, [])
100
+
101
+ const startTimer = useCallback(
102
+ (increment: boolean) => {
103
+ stopTimer()
104
+
105
+ // Initial click
106
+ const currentSize = typeof fontSizeRef.current === "number" ? fontSizeRef.current : DEFAULT_FONT_SIZE
107
+ updateFontSize(currentSize + (increment ? 1 : -1))
108
+
109
+ timerRef.current = setTimeout(() => {
110
+ intervalRef.current = setInterval(() => {
111
+ const currentSizeLoop = typeof fontSizeRef.current === "number" ? fontSizeRef.current : DEFAULT_FONT_SIZE
112
+ if (increment && currentSizeLoop < MAX_FONT_SIZE) {
113
+ updateFontSize(currentSizeLoop + 1)
114
+ } else if (!increment && currentSizeLoop > MIN_FONT_SIZE) {
115
+ updateFontSize(currentSizeLoop - 1)
116
+ } else {
117
+ stopTimer()
118
+ }
119
+ }, HOLD_INTERVAL)
120
+ }, HOLD_DELAY)
121
+ },
122
+ [updateFontSize, stopTimer]
123
+ )
124
+
125
+ useEffect(() => {
126
+ return () => stopTimer()
127
+ }, [stopTimer])
128
+
129
+ return (
130
+ <ButtonGroup>
131
+ <Button
132
+ variant="outline"
133
+ size="icon"
134
+ className="editor-toolbar-item"
135
+ onPointerDown={() => startTimer(false)}
136
+ onPointerUp={stopTimer}
137
+ onPointerLeave={stopTimer}
138
+ disabled={typeof fontSize === "number" && fontSize <= MIN_FONT_SIZE}
139
+ title="Decrease font size"
140
+ >
141
+ <IconSize size="xs">
142
+ <Minus />
143
+ </IconSize>
144
+ </Button>
145
+ <Input
146
+ type="number"
147
+ value={fontSize}
148
+ onChange={handleInputChange}
149
+ onBlur={handleInputBlur}
150
+ className="editor-toolbar-item editor-toolbar-item--w-fit editor-toolbar-item--bg-background editor-toolbar-item--text-center editor-w-14"
151
+ min={MIN_FONT_SIZE}
152
+ max={MAX_FONT_SIZE}
153
+ onKeyDown={(event) => {
154
+ if (event.key === "Enter") {
155
+ event.preventDefault()
156
+ handleInputBlur()
157
+ }
158
+ event.stopPropagation()
159
+ }}
160
+ />
161
+ <Button
162
+ variant="outline"
163
+ size="icon"
164
+ className="editor-toolbar-item"
165
+ onPointerDown={() => startTimer(true)}
166
+ onPointerUp={stopTimer}
167
+ onPointerLeave={stopTimer}
168
+ disabled={typeof fontSize === "number" && fontSize >= MAX_FONT_SIZE}
169
+ title="Increase font size"
170
+ >
171
+ <IconSize size="xs">
172
+ <Plus />
173
+ </IconSize>
174
+ </Button>
175
+ </ButtonGroup>
176
+ )
177
+ }
@@ -0,0 +1,87 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
+ import { IS_APPLE, mergeRegister } from "@lexical/utils"
6
+ import {
7
+ CAN_REDO_COMMAND,
8
+ CAN_UNDO_COMMAND,
9
+ COMMAND_PRIORITY_CRITICAL,
10
+ REDO_COMMAND,
11
+ UNDO_COMMAND,
12
+ } from "lexical"
13
+ import { RedoIcon, UndoIcon } from "lucide-react"
14
+
15
+ import { useToolbarContext } from "../../context/toolbar-context"
16
+ import { Button } from "../../ui/button"
17
+ import { ButtonGroup } from "../../ui/button-group"
18
+
19
+ export function HistoryToolbarPlugin() {
20
+ const [editor] = useLexicalComposerContext()
21
+ const { activeEditor, $updateToolbar } = useToolbarContext()
22
+ const [isEditable, setIsEditable] = useState(editor.isEditable())
23
+ const [canUndo, setCanUndo] = useState(false)
24
+ const [canRedo, setCanRedo] = useState(false)
25
+
26
+ useEffect(() => {
27
+ return mergeRegister(
28
+ editor.registerEditableListener((editable) => {
29
+ setIsEditable(editable)
30
+ }),
31
+ activeEditor.registerUpdateListener(({ editorState }) => {
32
+ editorState.read(() => {
33
+ $updateToolbar()
34
+ })
35
+ }),
36
+ activeEditor.registerCommand<boolean>(
37
+ CAN_UNDO_COMMAND,
38
+ (payload) => {
39
+ setCanUndo(payload)
40
+ return false
41
+ },
42
+ COMMAND_PRIORITY_CRITICAL
43
+ ),
44
+ activeEditor.registerCommand<boolean>(
45
+ CAN_REDO_COMMAND,
46
+ (payload) => {
47
+ setCanRedo(payload)
48
+ return false
49
+ },
50
+ COMMAND_PRIORITY_CRITICAL
51
+ )
52
+ )
53
+ }, [$updateToolbar, activeEditor, editor])
54
+
55
+ return (
56
+ <ButtonGroup>
57
+ <Button
58
+ disabled={!canUndo || !isEditable}
59
+ onClick={() => {
60
+ activeEditor.dispatchCommand(UNDO_COMMAND, undefined)
61
+ }}
62
+ title={IS_APPLE ? "Undo (⌘Z)" : "Undo (Ctrl+Z)"}
63
+ type="button"
64
+ aria-label="Undo"
65
+ size="sm"
66
+ variant="ghost"
67
+ className="editor-toolbar-item"
68
+ >
69
+ <UndoIcon className="editor-icon-sm" />
70
+ </Button>
71
+ <Button
72
+ disabled={!canRedo || !isEditable}
73
+ onClick={() => {
74
+ activeEditor.dispatchCommand(REDO_COMMAND, undefined)
75
+ }}
76
+ title={IS_APPLE ? "Redo (⇧⌘Z)" : "Redo (Ctrl+Y)"}
77
+ type="button"
78
+ aria-label="Redo"
79
+ size="sm"
80
+ variant="ghost"
81
+ className="editor-toolbar-item"
82
+ >
83
+ <RedoIcon className="editor-icon-sm" />
84
+ </Button>
85
+ </ButtonGroup>
86
+ )
87
+ }
@@ -0,0 +1,90 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useState } from "react"
4
+ import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"
5
+ import {
6
+ $isRangeSelection,
7
+ BaseSelection,
8
+ COMMAND_PRIORITY_NORMAL,
9
+ KEY_MODIFIER_COMMAND,
10
+ } from "lexical"
11
+ import { LinkIcon } from "lucide-react"
12
+
13
+ import { useToolbarContext } from "../../context/toolbar-context"
14
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
15
+ import { getSelectedNode } from "../../utils/get-selected-node"
16
+ import { sanitizeUrl } from "../../utils/url"
17
+ import { Toggle } from "../../ui/toggle"
18
+ import { IconSize } from "../../ui/typography"
19
+
20
+ export function LinkToolbarPlugin({
21
+ setIsLinkEditMode,
22
+ }: {
23
+ setIsLinkEditMode: (isEditMode: boolean) => void
24
+ }) {
25
+ const { activeEditor } = useToolbarContext()
26
+ const [isLink, setIsLink] = useState(false)
27
+
28
+ const $updateToolbar = (selection: BaseSelection) => {
29
+ if ($isRangeSelection(selection)) {
30
+ const node = getSelectedNode(selection)
31
+ const parent = node.getParent()
32
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
33
+ setIsLink(true)
34
+ } else {
35
+ setIsLink(false)
36
+ }
37
+ }
38
+ }
39
+
40
+ useUpdateToolbarHandler($updateToolbar)
41
+
42
+ useEffect(() => {
43
+ return activeEditor.registerCommand(
44
+ KEY_MODIFIER_COMMAND,
45
+ (payload) => {
46
+ const event: KeyboardEvent = payload
47
+ const { code, ctrlKey, metaKey } = event
48
+
49
+ if (code === "KeyK" && (ctrlKey || metaKey)) {
50
+ event.preventDefault()
51
+ let url: string | null
52
+ if (!isLink) {
53
+ setIsLinkEditMode(true)
54
+ url = sanitizeUrl("https://")
55
+ } else {
56
+ setIsLinkEditMode(false)
57
+ url = null
58
+ }
59
+ return activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, url)
60
+ }
61
+ return false
62
+ },
63
+ COMMAND_PRIORITY_NORMAL
64
+ )
65
+ }, [activeEditor, isLink, setIsLinkEditMode])
66
+
67
+ const insertLink = useCallback(() => {
68
+ if (!isLink) {
69
+ setIsLinkEditMode(true)
70
+ activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl("https://"))
71
+ } else {
72
+ setIsLinkEditMode(false)
73
+ activeEditor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
74
+ }
75
+ }, [activeEditor, isLink, setIsLinkEditMode])
76
+
77
+ return (
78
+ <Toggle
79
+ variant={"outline"}
80
+ size="sm"
81
+ className="editor-toolbar-item"
82
+ aria-label="Toggle link"
83
+ onClick={insertLink}
84
+ >
85
+ <IconSize size="sm">
86
+ <LinkIcon />
87
+ </IconSize>
88
+ </Toggle>
89
+ )
90
+ }
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { $isTableSelection } from "@lexical/table"
5
+ import { $isRangeSelection, BaseSelection, FORMAT_TEXT_COMMAND } from "lexical"
6
+ import { SubscriptIcon, SuperscriptIcon } from "lucide-react"
7
+
8
+ import { useToolbarContext } from "../../context/toolbar-context"
9
+ import { useUpdateToolbarHandler } from "../../editor-hooks/use-update-toolbar"
10
+ import {
11
+ ToggleGroup,
12
+ ToggleGroupItem,
13
+ } from "../../ui/toggle-group"
14
+ import { IconSize } from "../../ui/typography"
15
+
16
+ export function SubSuperToolbarPlugin() {
17
+ const { activeEditor } = useToolbarContext()
18
+ const [isSubscript, setIsSubscript] = useState(false)
19
+ const [isSuperscript, setIsSuperscript] = useState(false)
20
+
21
+ const $updateToolbar = (selection: BaseSelection) => {
22
+ if ($isRangeSelection(selection) || $isTableSelection(selection)) {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ setIsSubscript((selection as any).hasFormat("subscript"))
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ setIsSuperscript((selection as any).hasFormat("superscript"))
27
+ }
28
+ }
29
+
30
+ useUpdateToolbarHandler($updateToolbar)
31
+
32
+ return (
33
+ <ToggleGroup
34
+ type="single"
35
+ value={
36
+ isSubscript ? "subscript" : isSuperscript ? "superscript" : ""
37
+ }
38
+ size="default"
39
+ variant="outline"
40
+ >
41
+ <ToggleGroupItem
42
+ value="subscript"
43
+ size="default"
44
+ aria-label="Toggle subscript"
45
+ onClick={() => {
46
+ activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, "subscript")
47
+ }}
48
+ variant={"outline"}
49
+ >
50
+ <IconSize size="sm">
51
+ <SubscriptIcon />
52
+ </IconSize>
53
+ </ToggleGroupItem>
54
+ <ToggleGroupItem
55
+ value="superscript"
56
+ size="default"
57
+ aria-label="Toggle superscript"
58
+ onClick={() => {
59
+ activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, "superscript")
60
+ }}
61
+ variant={"outline"}
62
+ >
63
+ <IconSize size="sm">
64
+ <SuperscriptIcon />
65
+ </IconSize>
66
+ </ToggleGroupItem>
67
+ </ToggleGroup>
68
+ )
69
+ }