@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,499 @@
1
+ "use client"
2
+ import * as React from "react"
3
+ import { JSX, useRef } from "react"
4
+ import { calculateZoomLevel } from "@lexical/utils"
5
+ import type { LexicalEditor } from "lexical"
6
+ import { ImagePlus, ImageMinus, Maximize2, Minimize2, Trash2, AlignLeft, AlignCenter, AlignRight } from "lucide-react"
7
+
8
+ import { Button } from "../ui/button"
9
+ import {
10
+ getContainerWidth,
11
+ getImageAspectRatio,
12
+ } from "../editor-ui/image-sizing"
13
+ import { useEditorContainer } from "../context/editor-container-context"
14
+ import { IconSize } from "../ui/typography"
15
+
16
+ function clamp(value: number, min: number, max: number) {
17
+ // Nếu max là Infinity, chỉ giới hạn min
18
+ if (max === Infinity) {
19
+ return Math.max(value, min)
20
+ }
21
+ return Math.min(Math.max(value, min), max)
22
+ }
23
+
24
+ const Direction = {
25
+ east: 1 << 0,
26
+ north: 1 << 3,
27
+ south: 1 << 1,
28
+ west: 1 << 2,
29
+ }
30
+
31
+ const RESIZE_HANDLES = [
32
+ {
33
+ key: "n",
34
+ direction: Direction.north,
35
+ className: "editor-image-resizer-handle editor-image-resizer-handle--n",
36
+ },
37
+ {
38
+ key: "ne",
39
+ direction: Direction.north | Direction.east,
40
+ className: "editor-image-resizer-handle editor-image-resizer-handle--ne",
41
+ },
42
+ {
43
+ key: "e",
44
+ direction: Direction.east,
45
+ className: "editor-image-resizer-handle editor-image-resizer-handle--e",
46
+ },
47
+ {
48
+ key: "se",
49
+ direction: Direction.south | Direction.east,
50
+ className: "editor-image-resizer-handle editor-image-resizer-handle--se",
51
+ },
52
+ {
53
+ key: "s",
54
+ direction: Direction.south,
55
+ className: "editor-image-resizer-handle editor-image-resizer-handle--s",
56
+ },
57
+ {
58
+ key: "sw",
59
+ direction: Direction.south | Direction.west,
60
+ className: "editor-image-resizer-handle editor-image-resizer-handle--sw",
61
+ },
62
+ {
63
+ key: "w",
64
+ direction: Direction.west,
65
+ className: "editor-image-resizer-handle editor-image-resizer-handle--w",
66
+ },
67
+ {
68
+ key: "nw",
69
+ direction: Direction.north | Direction.west,
70
+ className: "editor-image-resizer-handle editor-image-resizer-handle--nw",
71
+ },
72
+ ] as const
73
+
74
+ export interface MediaResizerProps {
75
+ editor: LexicalEditor
76
+ buttonRef: { current: null | HTMLButtonElement }
77
+ mediaRef: { current: null | HTMLElement }
78
+ onResizeEnd: (width: "inherit" | number, height: "inherit" | number) => void
79
+ onResizeStart: () => void
80
+ showCaption?: boolean
81
+ setShowCaption?: (show: boolean) => void
82
+ captionsEnabled?: boolean
83
+ onSetFullWidth?: () => void
84
+ isFullWidth?: boolean
85
+ unlockBoundaries?: boolean
86
+ onReplaceMedia?: () => void
87
+ maxWidth?: number
88
+ onDelete?: () => void
89
+ onAlign?: (format: "left" | "center" | "right") => void
90
+ }
91
+
92
+ /**
93
+ * MediaResizer - Universal resizer for media elements (images, videos, etc.)
94
+ * Supports both images with captions and videos without captions
95
+ */
96
+ export function MediaResizer({
97
+ onResizeStart,
98
+ onResizeEnd,
99
+ buttonRef,
100
+ mediaRef,
101
+ editor,
102
+ showCaption = false,
103
+ setShowCaption,
104
+ captionsEnabled = false,
105
+ onSetFullWidth,
106
+ isFullWidth,
107
+ unlockBoundaries = true,
108
+ onReplaceMedia,
109
+ maxWidth,
110
+ onDelete,
111
+ onAlign,
112
+ }: MediaResizerProps): JSX.Element {
113
+ const controlWrapperRef = useRef<HTMLDivElement>(null)
114
+ const userSelect = useRef({
115
+ priority: "",
116
+ value: "default",
117
+ })
118
+ const positioningRef = useRef<{
119
+ currentHeight: "inherit" | number
120
+ currentWidth: "inherit" | number
121
+ direction: number
122
+ isResizing: boolean
123
+ ratio: number
124
+ startHeight: number
125
+ startWidth: number
126
+ startX: number
127
+ startY: number
128
+ maxWidthLimit: number
129
+ }>({
130
+ currentHeight: 0,
131
+ currentWidth: 0,
132
+ direction: 0,
133
+ isResizing: false,
134
+ ratio: 0,
135
+ startHeight: 0,
136
+ startWidth: 0,
137
+ startX: 0,
138
+ startY: 0,
139
+ maxWidthLimit: Infinity,
140
+ })
141
+ const editorRootElement = editor.getRootElement()
142
+ const maxHeightContainer = Infinity
143
+ const editorContainer = useEditorContainer()
144
+ const hardWidthLimit = maxWidth ?? editorContainer?.maxWidth
145
+
146
+ const minWidth = 100
147
+ const minHeight = 100
148
+
149
+ React.useEffect(() => {
150
+ // if (!unlockBoundaries) {
151
+ // return
152
+ // }
153
+ // if (mediaRef.current) {
154
+ // unlockImageBoundaries(mediaRef.current)
155
+ // }
156
+ }, [mediaRef, unlockBoundaries])
157
+
158
+ const setStartCursor = (direction: number) => {
159
+ const ew = direction === Direction.east || direction === Direction.west
160
+ const ns = direction === Direction.north || direction === Direction.south
161
+ const nwse =
162
+ (direction & Direction.north && direction & Direction.west) ||
163
+ (direction & Direction.south && direction & Direction.east)
164
+
165
+ const cursorDir = ew ? "ew" : ns ? "ns" : nwse ? "nwse" : "nesw"
166
+
167
+ if (editorRootElement !== null) {
168
+ editorRootElement.style.setProperty(
169
+ "cursor",
170
+ `${cursorDir}-resize`,
171
+ "important"
172
+ )
173
+ }
174
+ if (document.body !== null) {
175
+ document.body.style.setProperty(
176
+ "cursor",
177
+ `${cursorDir}-resize`,
178
+ "important"
179
+ )
180
+ userSelect.current.value = document.body.style.getPropertyValue(
181
+ "-webkit-user-select"
182
+ )
183
+ userSelect.current.priority = document.body.style.getPropertyPriority(
184
+ "-webkit-user-select"
185
+ )
186
+ document.body.style.setProperty(
187
+ "-webkit-user-select",
188
+ `none`,
189
+ "important"
190
+ )
191
+ }
192
+ }
193
+
194
+ const setEndCursor = () => {
195
+ if (editorRootElement !== null) {
196
+ editorRootElement.style.setProperty("cursor", "text")
197
+ }
198
+ if (document.body !== null) {
199
+ document.body.style.setProperty("cursor", "default")
200
+ document.body.style.setProperty(
201
+ "-webkit-user-select",
202
+ userSelect.current.value,
203
+ userSelect.current.priority
204
+ )
205
+ }
206
+ }
207
+
208
+ const handlePointerDown = (
209
+ event: React.PointerEvent<HTMLDivElement>,
210
+ direction: number
211
+ ) => {
212
+ if (!editor.isEditable()) {
213
+ return
214
+ }
215
+
216
+ const media = mediaRef.current
217
+ const controlWrapper = controlWrapperRef.current
218
+
219
+ if (media !== null && controlWrapper !== null) {
220
+ event.preventDefault()
221
+ // if (unlockBoundaries) {
222
+ // unlockImageBoundaries(media)
223
+ // }
224
+ const { width, height } = media.getBoundingClientRect()
225
+ const zoom = calculateZoomLevel(media)
226
+ const positioning = positioningRef.current
227
+ positioning.startWidth = width
228
+ positioning.startHeight = height
229
+ positioning.ratio = getImageAspectRatio(media)
230
+ positioning.currentWidth = width
231
+ positioning.currentHeight = height
232
+ positioning.startX = event.clientX / zoom
233
+ positioning.startY = event.clientY / zoom
234
+ positioning.isResizing = true
235
+ positioning.direction = direction
236
+
237
+ const containerWidth = getContainerWidth(media, editorRootElement, hardWidthLimit)
238
+ positioning.maxWidthLimit = containerWidth || hardWidthLimit || Infinity
239
+
240
+ setStartCursor(direction)
241
+ onResizeStart()
242
+
243
+ controlWrapper.classList.add("touch-action-none")
244
+ media.style.height = `${height}px`
245
+ media.style.width = `${width}px`
246
+
247
+ document.addEventListener("pointermove", handlePointerMove)
248
+ document.addEventListener("pointerup", handlePointerUp)
249
+ }
250
+ }
251
+ const getEffectiveMaxWidth = () => {
252
+ const maxFromPositioning =
253
+ typeof positioningRef.current.maxWidthLimit === "number"
254
+ ? positioningRef.current.maxWidthLimit
255
+ : Infinity
256
+ const maxFromContext =
257
+ typeof hardWidthLimit === "number" ? hardWidthLimit : Infinity
258
+ return Math.min(maxFromPositioning, maxFromContext)
259
+ }
260
+ const handlePointerMove = (event: PointerEvent) => {
261
+ const media = mediaRef.current
262
+ const positioning = positioningRef.current
263
+
264
+ const isHorizontal =
265
+ positioning.direction & (Direction.east | Direction.west)
266
+ const isVertical =
267
+ positioning.direction & (Direction.south | Direction.north)
268
+
269
+ if (media !== null && positioning.isResizing) {
270
+ const zoom = calculateZoomLevel(media)
271
+ // Corner cursor
272
+ if (isHorizontal && isVertical) {
273
+ let diff = Math.floor(positioning.startX - event.clientX / zoom)
274
+ diff = positioning.direction & Direction.east ? -diff : diff
275
+
276
+ const effectiveMaxWidth = getEffectiveMaxWidth()
277
+ const width = clamp(
278
+ positioning.startWidth + diff,
279
+ minWidth,
280
+ effectiveMaxWidth || Infinity
281
+ )
282
+ const widthReachedLimit =
283
+ effectiveMaxWidth !== Infinity &&
284
+ width >= effectiveMaxWidth &&
285
+ diff > 0
286
+
287
+ const height = width / positioning.ratio
288
+ media.style.width = `${width}px`
289
+ media.style.height = `${height}px`
290
+ positioning.currentHeight = height
291
+ positioning.currentWidth = width
292
+ if (widthReachedLimit) {
293
+ return
294
+ }
295
+ } else if (isVertical) {
296
+ let diff = Math.floor(positioning.startY - event.clientY / zoom)
297
+ diff = positioning.direction & Direction.south ? -diff : diff
298
+
299
+ const height = clamp(
300
+ positioning.startHeight + diff,
301
+ minHeight,
302
+ maxHeightContainer
303
+ )
304
+
305
+ // Calculate width based on aspect ratio to maintain proportions
306
+ const width = height * positioning.ratio
307
+ media.style.height = `${height}px`
308
+ media.style.width = `${width}px`
309
+ positioning.currentHeight = height
310
+ positioning.currentWidth = width
311
+ } else {
312
+ let diff = Math.floor(positioning.startX - event.clientX / zoom)
313
+ diff = positioning.direction & Direction.east ? -diff : diff
314
+
315
+ const effectiveMaxWidth = getEffectiveMaxWidth()
316
+ const width = clamp(
317
+ positioning.startWidth + diff,
318
+ minWidth,
319
+ effectiveMaxWidth || Infinity
320
+ )
321
+ const widthReachedLimit =
322
+ effectiveMaxWidth !== Infinity &&
323
+ width >= effectiveMaxWidth &&
324
+ diff > 0
325
+
326
+ // Calculate height based on aspect ratio to maintain proportions
327
+ const height = width / positioning.ratio
328
+ media.style.width = `${width}px`
329
+ media.style.height = `${height}px`
330
+ positioning.currentWidth = width
331
+ positioning.currentHeight = height
332
+ if (widthReachedLimit) {
333
+ return
334
+ }
335
+ }
336
+ }
337
+ }
338
+ const handlePointerUp = () => {
339
+ const media = mediaRef.current
340
+ const positioning = positioningRef.current
341
+ const controlWrapper = controlWrapperRef.current
342
+ if (media !== null && controlWrapper !== null && positioning.isResizing) {
343
+ const width = positioning.currentWidth
344
+ const height = positioning.currentHeight
345
+ positioning.startWidth = 0
346
+ positioning.startHeight = 0
347
+ positioning.ratio = 0
348
+ positioning.startX = 0
349
+ positioning.startY = 0
350
+ positioning.currentWidth = 0
351
+ positioning.currentHeight = 0
352
+ positioning.isResizing = false
353
+
354
+ controlWrapper.classList.remove("touch-action-none")
355
+
356
+ setEndCursor()
357
+ onResizeEnd(width, height)
358
+
359
+ document.removeEventListener("pointermove", handlePointerMove)
360
+ document.removeEventListener("pointerup", handlePointerUp)
361
+ }
362
+ }
363
+
364
+ return (
365
+ <>
366
+ <div ref={controlWrapperRef}>
367
+ {RESIZE_HANDLES.map(({ key, direction, className }) => (
368
+ <div
369
+ key={key}
370
+ className={className}
371
+ onPointerDown={(event) => {
372
+ handlePointerDown(event, direction)
373
+ }}
374
+ />
375
+ ))}
376
+ </div>
377
+ <div className="editor-mt-2 editor-flex editor-flex-wrap editor-justify-center editor-gap-2">
378
+ {onDelete && (
379
+ <Button
380
+ className="editor-image-delete-button"
381
+ type="button"
382
+ variant="outline"
383
+ size="sm"
384
+ onClick={onDelete}
385
+ >
386
+ <IconSize size="sm" className="editor-mr-1-5">
387
+ <Trash2 />
388
+ </IconSize>
389
+ Delete
390
+ </Button>
391
+ )}
392
+ {onAlign && (
393
+ <div className="editor-flex editor-gap-1">
394
+ <Button
395
+ className="editor-image-align-left-button"
396
+ type="button"
397
+ variant="outline"
398
+ size="sm"
399
+ onClick={() => onAlign("left")}
400
+ >
401
+ <IconSize size="sm">
402
+ <AlignLeft />
403
+ </IconSize>
404
+ </Button>
405
+ <Button
406
+ className="editor-image-align-center-button"
407
+ type="button"
408
+ variant="outline"
409
+ size="sm"
410
+ onClick={() => onAlign("center")}
411
+ >
412
+ <IconSize size="sm">
413
+ <AlignCenter />
414
+ </IconSize>
415
+ </Button>
416
+ <Button
417
+ className="editor-image-align-right-button"
418
+ type="button"
419
+ variant="outline"
420
+ size="sm"
421
+ onClick={() => onAlign("right")}
422
+ >
423
+ <IconSize size="sm">
424
+ <AlignRight />
425
+ </IconSize>
426
+ </Button>
427
+ </div>
428
+ )}
429
+ {onReplaceMedia && (
430
+ <Button
431
+ className="editor-image-replace-button"
432
+ type="button"
433
+ variant="outline"
434
+ size="sm"
435
+ onClick={onReplaceMedia}
436
+ >
437
+ Change Image
438
+ </Button>
439
+ )}
440
+ {onSetFullWidth && (
441
+ <Button
442
+ className="editor-image-full-width-button"
443
+ type="button"
444
+ variant="outline"
445
+ size="sm"
446
+ onClick={onSetFullWidth}
447
+ >
448
+ {isFullWidth ? (
449
+ <>
450
+ <IconSize size="sm" className="editor-mr-1-5">
451
+ <Minimize2 />
452
+ </IconSize>
453
+ Reset Width
454
+ </>
455
+ ) : (
456
+ <>
457
+ <IconSize size="sm" className="editor-mr-1-5">
458
+ <Maximize2 />
459
+ </IconSize>
460
+ Full Width
461
+ </>
462
+ )}
463
+ </Button>
464
+ )}
465
+ {setShowCaption && captionsEnabled && (
466
+ <Button
467
+ className="editor-image-caption-button"
468
+ ref={buttonRef}
469
+ type="button"
470
+ variant={"outline"}
471
+ size="sm"
472
+ onClick={() => {
473
+ setShowCaption(!showCaption)
474
+ }}
475
+ >
476
+ {showCaption ? (
477
+ <>
478
+ <IconSize size="sm" className="editor-mr-1-5">
479
+ <ImageMinus />
480
+ </IconSize>
481
+ Remove Caption
482
+ </>
483
+ ) : (
484
+ <>
485
+ <IconSize size="sm" className="editor-mr-1-5">
486
+ <ImagePlus />
487
+ </IconSize>
488
+ Add Caption
489
+ </>
490
+ )}
491
+ </Button>
492
+ )}
493
+ </div>
494
+ </>
495
+ )
496
+ }
497
+
498
+ // Backward compatibility: export as ImageResizer for existing code
499
+ export const ImageResizer = MediaResizer
@@ -0,0 +1,120 @@
1
+ export const unlockImageBoundaries = (image: HTMLElement) => {
2
+ // Allow resizing beyond container width
3
+ image.style.setProperty("max-width", "none", "important")
4
+ image.style.setProperty("max-height", "none", "important")
5
+ }
6
+
7
+ export const getImageAspectRatio = (image: HTMLElement | null) => {
8
+ if (!image) {
9
+ return 1
10
+ }
11
+ // HTMLImageElement has naturalWidth and naturalHeight properties
12
+ const imgElement = image as HTMLImageElement
13
+ if (imgElement.naturalWidth > 0 && imgElement.naturalHeight > 0) {
14
+ return imgElement.naturalWidth / imgElement.naturalHeight
15
+ }
16
+ const { width, height } = image.getBoundingClientRect()
17
+ if (!height) {
18
+ return 1
19
+ }
20
+ return width / height
21
+ }
22
+
23
+ export const getInnerWidth = (element: HTMLElement) => {
24
+ const rectWidth = element.getBoundingClientRect().width
25
+ if (rectWidth <= 0) {
26
+ return 0
27
+ }
28
+ if (typeof window === "undefined") {
29
+ return rectWidth
30
+ }
31
+ const styles = window.getComputedStyle(element)
32
+ const paddingLeft = Number.parseFloat(styles.paddingLeft || "0") || 0
33
+ const paddingRight = Number.parseFloat(styles.paddingRight || "0") || 0
34
+ return Math.max(rectWidth - (paddingLeft + paddingRight), 0)
35
+ }
36
+
37
+ export const getNearestContentWidth = (image: HTMLElement) => {
38
+ if (typeof window === "undefined") {
39
+ return null
40
+ }
41
+
42
+ let current: HTMLElement | null = image.parentElement
43
+ while (current) {
44
+ const display = window.getComputedStyle(current).display
45
+ const isInline =
46
+ display === "inline" ||
47
+ display === "inline-block" ||
48
+ display === "inline-flex" ||
49
+ display === "contents"
50
+
51
+ if (!isInline) {
52
+ const width = getInnerWidth(current)
53
+ if (width > 0) {
54
+ return width
55
+ }
56
+ }
57
+ current = current.parentElement
58
+ }
59
+
60
+ return null
61
+ }
62
+
63
+ const FIELD_CONTENT_SELECTOR = "[data-slot='field-content']"
64
+
65
+ const getFieldContentWidth = (element: HTMLElement | null) => {
66
+ if (!element) {
67
+ return null
68
+ }
69
+ const fieldContent = element.closest(FIELD_CONTENT_SELECTOR) as
70
+ | HTMLElement
71
+ | null
72
+ if (fieldContent) {
73
+ const width = getInnerWidth(fieldContent)
74
+ if (width > 0) {
75
+ return width
76
+ }
77
+ }
78
+ return null
79
+ }
80
+
81
+ const clampWidth = (value: number | null, hardLimit?: number) => {
82
+ if (!value || value <= 0) {
83
+ return null
84
+ }
85
+ if (hardLimit && hardLimit > 0) {
86
+ return Math.min(value, hardLimit)
87
+ }
88
+ return value
89
+ }
90
+
91
+ export const getContainerWidth = (
92
+ element: HTMLElement,
93
+ editorRoot: HTMLElement | null,
94
+ hardLimit?: number
95
+ ) => {
96
+ const fieldContentWidth = clampWidth(
97
+ getFieldContentWidth(element),
98
+ hardLimit
99
+ )
100
+ if (fieldContentWidth) {
101
+ return fieldContentWidth
102
+ }
103
+ const contentWidth = clampWidth(getNearestContentWidth(element), hardLimit)
104
+ if (contentWidth) {
105
+ return contentWidth
106
+ }
107
+
108
+ if (editorRoot) {
109
+ const rootWidth = clampWidth(getInnerWidth(editorRoot), hardLimit)
110
+ if (rootWidth) {
111
+ return rootWidth
112
+ }
113
+ }
114
+
115
+ const elementWidth = clampWidth(
116
+ element.getBoundingClientRect().width,
117
+ hardLimit
118
+ )
119
+ return elementWidth ?? undefined
120
+ }