@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,37 @@
1
+ import { JSX } from "react"
2
+ import { ContentEditable as LexicalContentEditable } from "@lexical/react/LexicalContentEditable"
3
+ import { cn } from "../lib/utils"
4
+
5
+ type Props = {
6
+ placeholder: string
7
+ className?: string
8
+ placeholderClassName?: string
9
+ placeholderDefaults?: boolean // apply default positioning/padding for placeholder
10
+ }
11
+
12
+ export function ContentEditable({
13
+ placeholder,
14
+ className,
15
+ placeholderClassName,
16
+ placeholderDefaults = true,
17
+ }: Props): JSX.Element {
18
+ return (
19
+ <LexicalContentEditable
20
+ className={cn("ContentEditable__root relative block min-h-72 px-8 py-4 focus:outline-none", className)}
21
+ aria-placeholder={placeholder}
22
+ aria-label={placeholder || "Editor nội dung"}
23
+ placeholder={
24
+ <div
25
+ className={cn(
26
+ placeholderClassName,
27
+ "text-muted-foreground pointer-events-none select-none",
28
+ placeholderDefaults &&
29
+ "absolute top-0 left-0 overflow-hidden px-8 py-[18px] text-ellipsis"
30
+ )}
31
+ >
32
+ {placeholder}
33
+ </div>
34
+ }
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,118 @@
1
+ "use client"
2
+ import { useState, useEffect, useRef, useCallback } from "react"
3
+ import { LexicalEditor, NodeKey, $getNodeByKey, $getRoot } from "lexical"
4
+ import { $isImageNode } from "../../nodes/image-node"
5
+
6
+ interface UseImageCaptionControlsProps {
7
+ caption: LexicalEditor
8
+ editor: LexicalEditor
9
+ nodeKey: NodeKey
10
+ showCaption: boolean
11
+ }
12
+
13
+ type TimeoutHandle = ReturnType<typeof setTimeout>
14
+
15
+ /**
16
+ * Custom hook to handle image caption logic.
17
+ * Manages caption visibility and content synchronization.
18
+ */
19
+ export function useImageCaptionControls({
20
+ caption,
21
+ editor,
22
+ nodeKey,
23
+ showCaption,
24
+ }: UseImageCaptionControlsProps) {
25
+ const [hasCaptionContent, setHasCaptionContent] = useState(false)
26
+ const [localShowCaption, setLocalShowCaption] = useState(showCaption)
27
+ const isUpdatingCaptionRef = useRef(false)
28
+ const lastShowCaptionRef = useRef(showCaption)
29
+
30
+ useEffect(() => {
31
+ let timeout: TimeoutHandle | null = null
32
+ timeout = setTimeout(() => {
33
+ if (!showCaption) {
34
+ setHasCaptionContent(false)
35
+ setLocalShowCaption(false)
36
+ } else if (
37
+ !isUpdatingCaptionRef.current &&
38
+ lastShowCaptionRef.current !== showCaption
39
+ ) {
40
+ setLocalShowCaption(showCaption)
41
+ lastShowCaptionRef.current = showCaption
42
+ }
43
+ isUpdatingCaptionRef.current = false
44
+ }, 0)
45
+
46
+ return () => {
47
+ if (timeout) {
48
+ clearTimeout(timeout)
49
+ }
50
+ }
51
+ }, [showCaption])
52
+
53
+ const setShowCaption = useCallback(
54
+ (show: boolean) => {
55
+ isUpdatingCaptionRef.current = true
56
+ lastShowCaptionRef.current = show
57
+
58
+ setLocalShowCaption(show)
59
+
60
+ if (!show) {
61
+ setHasCaptionContent(false)
62
+ }
63
+
64
+ editor.update(() => {
65
+ const node = $getNodeByKey(nodeKey)
66
+ if ($isImageNode(node)) {
67
+ node.setShowCaption(show)
68
+
69
+ if (!show) {
70
+ caption.update(() => {
71
+ const root = $getRoot()
72
+ root.clear()
73
+ })
74
+ }
75
+ }
76
+ })
77
+ },
78
+ [caption, editor, nodeKey]
79
+ )
80
+
81
+ useEffect(() => {
82
+ const computeHasContent = () => {
83
+ const state = caption.getEditorState()
84
+ const hasContent = state.read(() => {
85
+ const root = $getRoot()
86
+ const raw = root.getTextContent()
87
+ const text = raw.replace(/[\u200B\u00A0\s]+/g, "")
88
+ return text.length > 0
89
+ })
90
+ setHasCaptionContent(showCaption && hasContent)
91
+ }
92
+
93
+ const timer = setTimeout(() => {
94
+ computeHasContent()
95
+ }, 0)
96
+
97
+ const unregister = caption.registerUpdateListener(() => {
98
+ computeHasContent()
99
+ editor.update(() => {
100
+ const node = $getNodeByKey(nodeKey)
101
+ if ($isImageNode(node)) {
102
+ node.setShowCaption(showCaption)
103
+ }
104
+ })
105
+ })
106
+
107
+ return () => {
108
+ clearTimeout(timer)
109
+ unregister()
110
+ }
111
+ }, [caption, editor, nodeKey, showCaption])
112
+
113
+ return {
114
+ hasCaptionContent,
115
+ localShowCaption,
116
+ setShowCaption,
117
+ }
118
+ }
@@ -0,0 +1,245 @@
1
+ "use client"
2
+ import { useState, useEffect, useRef, useCallback } from "react"
3
+ import {
4
+ LexicalEditor,
5
+ NodeKey,
6
+ BaseSelection,
7
+ $getSelection,
8
+ $isNodeSelection,
9
+ $setSelection,
10
+ SELECTION_CHANGE_COMMAND,
11
+ CLICK_COMMAND,
12
+ DRAGSTART_COMMAND,
13
+ KEY_DELETE_COMMAND,
14
+ KEY_BACKSPACE_COMMAND,
15
+ KEY_ENTER_COMMAND,
16
+ KEY_ESCAPE_COMMAND,
17
+ COMMAND_PRIORITY_LOW,
18
+ createCommand,
19
+ LexicalCommand,
20
+ $isRangeSelection
21
+ } from "lexical"
22
+ import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection"
23
+ import { mergeRegister } from "@lexical/utils"
24
+ import { $isImageNode } from "../../nodes/image-node"
25
+
26
+ export const RIGHT_CLICK_IMAGE_COMMAND: LexicalCommand<MouseEvent> = createCommand("RIGHT_CLICK_IMAGE_COMMAND")
27
+
28
+ interface UseImageNodeInteractionsProps {
29
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>
30
+ caption: LexicalEditor
31
+ editor: LexicalEditor
32
+ imageRef: React.MutableRefObject<HTMLImageElement | null>
33
+ isResizing: boolean
34
+ nodeKey: NodeKey
35
+ showCaption: boolean
36
+ }
37
+
38
+ /**
39
+ * Custom hook to handle interactions with the image node.
40
+ * Handles selection, deletion, keyboard navigation, and context menu.
41
+ */
42
+ export function useImageNodeInteractions({
43
+ buttonRef,
44
+ caption,
45
+ editor,
46
+ imageRef,
47
+ isResizing,
48
+ nodeKey,
49
+ showCaption,
50
+ }: UseImageNodeInteractionsProps) {
51
+ const [selection, setSelection] = useState<BaseSelection | null>(null)
52
+ const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
53
+ const activeEditorRef = useRef<LexicalEditor | null>(null)
54
+
55
+ const $onDelete = useCallback(
56
+ (payload: KeyboardEvent) => {
57
+ const deleteSelection = $getSelection()
58
+ if (isSelected && $isNodeSelection(deleteSelection)) {
59
+ const event: KeyboardEvent = payload
60
+ event.preventDefault()
61
+ editor.update(() => {
62
+ deleteSelection.getNodes().forEach((node) => {
63
+ if ($isImageNode(node)) {
64
+ node.remove()
65
+ }
66
+ })
67
+ })
68
+ return true
69
+ }
70
+ return false
71
+ },
72
+ [editor, isSelected]
73
+ )
74
+
75
+ const $onEnter = useCallback(
76
+ (event: KeyboardEvent) => {
77
+ const latestSelection = $getSelection()
78
+ const buttonElem = buttonRef.current
79
+ if (
80
+ isSelected &&
81
+ $isNodeSelection(latestSelection) &&
82
+ latestSelection.getNodes().length === 1
83
+ ) {
84
+ if (showCaption) {
85
+ $setSelection(null)
86
+ event.preventDefault()
87
+ caption.focus()
88
+ return true
89
+ } else if (
90
+ buttonElem !== null &&
91
+ buttonElem !== document.activeElement
92
+ ) {
93
+ event.preventDefault()
94
+ buttonElem.focus()
95
+ return true
96
+ }
97
+ }
98
+ return false
99
+ },
100
+ [buttonRef, caption, isSelected, showCaption]
101
+ )
102
+
103
+ const $onEscape = useCallback(
104
+ (event: KeyboardEvent) => {
105
+ if (
106
+ activeEditorRef.current === caption ||
107
+ buttonRef.current === event.target
108
+ ) {
109
+ $setSelection(null)
110
+ editor.update(() => {
111
+ setSelected(true)
112
+ const parentRootElement = editor.getRootElement()
113
+ if (parentRootElement !== null) {
114
+ parentRootElement.focus()
115
+ }
116
+ })
117
+ return true
118
+ }
119
+ return false
120
+ },
121
+ [buttonRef, caption, editor, setSelected]
122
+ )
123
+
124
+ const onClick = useCallback(
125
+ (payload: MouseEvent) => {
126
+ const event = payload
127
+
128
+ if (isResizing) {
129
+ return true
130
+ }
131
+ if (event.target === imageRef.current) {
132
+ if (event.shiftKey) {
133
+ setSelected(!isSelected)
134
+ } else {
135
+ clearSelection()
136
+ setSelected(true)
137
+ }
138
+ return true
139
+ }
140
+
141
+ return false
142
+ },
143
+ [clearSelection, imageRef, isResizing, isSelected, setSelected]
144
+ )
145
+
146
+ const onRightClick = useCallback(
147
+ (event: MouseEvent): void => {
148
+ editor.getEditorState().read(() => {
149
+ const latestSelection = $getSelection()
150
+ const domElement = event.target as HTMLElement
151
+ if (
152
+ domElement.tagName === "IMG" &&
153
+ $isRangeSelection(latestSelection) &&
154
+ latestSelection.getNodes().length === 1
155
+ ) {
156
+ editor.dispatchCommand(
157
+ RIGHT_CLICK_IMAGE_COMMAND,
158
+ event as MouseEvent
159
+ )
160
+ }
161
+ })
162
+ },
163
+ [editor]
164
+ )
165
+
166
+ useEffect(() => {
167
+ let isMounted = true
168
+ const rootElement = editor.getRootElement()
169
+ const unregister = mergeRegister(
170
+ editor.registerUpdateListener(({ editorState }) => {
171
+ if (isMounted) {
172
+ setSelection(editorState.read(() => $getSelection()))
173
+ }
174
+ }),
175
+ editor.registerCommand(
176
+ SELECTION_CHANGE_COMMAND,
177
+ (_, activeEditor) => {
178
+ activeEditorRef.current = activeEditor
179
+ return false
180
+ },
181
+ COMMAND_PRIORITY_LOW
182
+ ),
183
+ editor.registerCommand<MouseEvent>(
184
+ CLICK_COMMAND,
185
+ onClick,
186
+ COMMAND_PRIORITY_LOW
187
+ ),
188
+ editor.registerCommand<MouseEvent>(
189
+ RIGHT_CLICK_IMAGE_COMMAND,
190
+ onClick,
191
+ COMMAND_PRIORITY_LOW
192
+ ),
193
+ editor.registerCommand(
194
+ DRAGSTART_COMMAND,
195
+ (event) => {
196
+ if (event.target === imageRef.current) {
197
+ event.preventDefault()
198
+ return true
199
+ }
200
+ return false
201
+ },
202
+ COMMAND_PRIORITY_LOW
203
+ ),
204
+ editor.registerCommand(
205
+ KEY_DELETE_COMMAND,
206
+ $onDelete,
207
+ COMMAND_PRIORITY_LOW
208
+ ),
209
+ editor.registerCommand(
210
+ KEY_BACKSPACE_COMMAND,
211
+ $onDelete,
212
+ COMMAND_PRIORITY_LOW
213
+ ),
214
+ editor.registerCommand(KEY_ENTER_COMMAND, $onEnter, COMMAND_PRIORITY_LOW),
215
+ editor.registerCommand(
216
+ KEY_ESCAPE_COMMAND,
217
+ $onEscape,
218
+ COMMAND_PRIORITY_LOW
219
+ )
220
+ )
221
+
222
+ rootElement?.addEventListener("contextmenu", onRightClick)
223
+
224
+ return () => {
225
+ isMounted = false
226
+ unregister()
227
+ rootElement?.removeEventListener("contextmenu", onRightClick)
228
+ }
229
+ }, [
230
+ clearSelection,
231
+ editor,
232
+ imageRef,
233
+ isResizing,
234
+ isSelected,
235
+ nodeKey,
236
+ $onDelete,
237
+ $onEnter,
238
+ $onEscape,
239
+ onClick,
240
+ onRightClick,
241
+ setSelected,
242
+ ])
243
+
244
+ return { isSelected, selection, setSelected, clearSelection }
245
+ }
@@ -0,0 +1,202 @@
1
+ "use client"
2
+ import { useState, useEffect } from "react"
3
+ import { LexicalEditor } from "lexical"
4
+ import {
5
+ getContainerWidth,
6
+ getImageAspectRatio
7
+ } from "../image-sizing"
8
+
9
+ export type DimensionValue = "inherit" | number
10
+
11
+ export const imageCache = new Map<string, { width: number; height: number; ratio: number }>()
12
+ export const DEFAULT_ASPECT_RATIO = 16 / 9
13
+ export const DEFAULT_WIDTH = 800
14
+ export const DEFAULT_HEIGHT = Math.round(DEFAULT_WIDTH / DEFAULT_ASPECT_RATIO)
15
+ export const DEFAULT_DIMENSIONS = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, ratio: DEFAULT_ASPECT_RATIO }
16
+
17
+ interface UseResponsiveImageDimensionsProps {
18
+ editor: LexicalEditor
19
+ height: DimensionValue
20
+ imageRef: { current: null | HTMLImageElement }
21
+ width: DimensionValue
22
+ isResizing: boolean
23
+ fullWidth?: boolean
24
+ maxWidthLimit?: number
25
+ src: string
26
+ }
27
+
28
+ /**
29
+ * Custom hook to handle responsive image dimensions.
30
+ * Calculates width and height based on container width and image aspect ratio.
31
+ */
32
+ export function useResponsiveImageDimensions({
33
+ editor,
34
+ height,
35
+ imageRef,
36
+ width,
37
+ isResizing,
38
+ fullWidth,
39
+ maxWidthLimit,
40
+ src,
41
+ }: UseResponsiveImageDimensionsProps) {
42
+ const [dimensions, setDimensions] = useState<{
43
+ width: DimensionValue
44
+ height: DimensionValue
45
+ }>({
46
+ width,
47
+ height,
48
+ })
49
+
50
+ useEffect(() => {
51
+ if (isResizing) {
52
+ return
53
+ }
54
+
55
+ let cancelScheduledUpdate: null | (() => void) = null
56
+ const cleanupTasks: Array<() => void> = []
57
+
58
+ const scheduleDimensionsUpdate = (next: {
59
+ width: DimensionValue
60
+ height: DimensionValue
61
+ }) => {
62
+ if (cancelScheduledUpdate) {
63
+ cancelScheduledUpdate()
64
+ }
65
+
66
+ const applyNext = () => {
67
+ cancelScheduledUpdate = null
68
+ setDimensions((prev) => {
69
+ if (prev.width === next.width && prev.height === next.height) {
70
+ return prev
71
+ }
72
+ return next
73
+ })
74
+ }
75
+
76
+ if (
77
+ typeof window !== "undefined" &&
78
+ typeof window.requestAnimationFrame === "function"
79
+ ) {
80
+ const frameId = window.requestAnimationFrame(applyNext)
81
+ cancelScheduledUpdate = () => window.cancelAnimationFrame(frameId)
82
+ } else {
83
+ const timeoutId = setTimeout(applyNext)
84
+ cancelScheduledUpdate = () => clearTimeout(timeoutId)
85
+ }
86
+ }
87
+
88
+ const editorRoot = editor.getRootElement()
89
+ const image = imageRef.current
90
+
91
+ if (fullWidth) {
92
+ scheduleDimensionsUpdate({ width: "inherit", height: "inherit" })
93
+ return () => {
94
+ cancelScheduledUpdate?.()
95
+ }
96
+ }
97
+
98
+ if (!image) {
99
+ scheduleDimensionsUpdate({ width, height })
100
+ return () => {
101
+ cancelScheduledUpdate?.()
102
+ }
103
+ }
104
+
105
+
106
+
107
+
108
+
109
+ const updateDimensions = () => {
110
+ if (fullWidth) {
111
+ scheduleDimensionsUpdate({ width: "inherit", height: "inherit" })
112
+ return
113
+ }
114
+
115
+ const containerWidth = getContainerWidth(
116
+ image,
117
+ editorRoot,
118
+ maxWidthLimit
119
+ )
120
+
121
+ if (!containerWidth) {
122
+ scheduleDimensionsUpdate({ width, height })
123
+ return
124
+ }
125
+
126
+ const cached = imageCache.get(src)
127
+ const baseWidth =
128
+ typeof width === "number"
129
+ ? width
130
+ : image.naturalWidth || cached?.width || image.getBoundingClientRect().width || containerWidth
131
+
132
+ let baseHeight: DimensionValue
133
+ if (typeof height === "number") {
134
+ baseHeight = height
135
+ } else if (image.naturalHeight > 0) {
136
+ baseHeight = image.naturalHeight
137
+ } else if (cached?.height) {
138
+ baseHeight = cached.height
139
+ } else {
140
+ const ratio = cached?.ratio || getImageAspectRatio(image) || DEFAULT_ASPECT_RATIO
141
+ baseHeight = ratio > 0 ? Math.round(baseWidth / ratio) : "inherit"
142
+ }
143
+
144
+ let nextWidth: DimensionValue = baseWidth
145
+ let nextHeight: DimensionValue = baseHeight
146
+
147
+ if (
148
+ width === "inherit" &&
149
+ typeof baseWidth === "number" &&
150
+ baseWidth > containerWidth
151
+ ) {
152
+ const scale = containerWidth / baseWidth
153
+ nextWidth = containerWidth
154
+ if (typeof baseHeight === "number") {
155
+ nextHeight = Math.max(Math.round(baseHeight * scale), 1)
156
+ } else if (baseHeight === "inherit") {
157
+ const ratio = cached?.ratio || getImageAspectRatio(image) || DEFAULT_ASPECT_RATIO
158
+ nextHeight =
159
+ ratio > 0 ? Math.max(Math.round(containerWidth / ratio), 1) : baseHeight
160
+ }
161
+ }
162
+
163
+ scheduleDimensionsUpdate({ width: nextWidth, height: nextHeight })
164
+ }
165
+
166
+ updateDimensions()
167
+
168
+ if (typeof ResizeObserver !== "undefined") {
169
+ const resizeObserver = new ResizeObserver(updateDimensions)
170
+ if (editorRoot) {
171
+ resizeObserver.observe(editorRoot)
172
+ }
173
+ if (image.parentElement) {
174
+ resizeObserver.observe(image.parentElement)
175
+ }
176
+ cleanupTasks.push(() => {
177
+ resizeObserver.disconnect()
178
+ })
179
+ } else if (typeof window !== "undefined") {
180
+ window.addEventListener("resize", updateDimensions)
181
+ cleanupTasks.push(() => {
182
+ window.removeEventListener("resize", updateDimensions)
183
+ })
184
+ }
185
+
186
+ return () => {
187
+ cancelScheduledUpdate?.()
188
+ cleanupTasks.forEach((task) => task())
189
+ }
190
+ }, [
191
+ editor,
192
+ height,
193
+ imageRef,
194
+ isResizing,
195
+ width,
196
+ fullWidth,
197
+ maxWidthLimit,
198
+ src,
199
+ ])
200
+
201
+ return dimensions
202
+ }