@thangph2146/lexical-editor 0.0.4 → 0.0.6

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 (101) hide show
  1. package/README.md +47 -0
  2. package/dist/editor-x/editor.cjs +732 -443
  3. package/dist/editor-x/editor.cjs.map +1 -1
  4. package/dist/editor-x/editor.css +1418 -1120
  5. package/dist/editor-x/editor.css.map +1 -1
  6. package/dist/editor-x/editor.d.cts +2 -1
  7. package/dist/editor-x/editor.d.ts +2 -1
  8. package/dist/editor-x/editor.js +736 -447
  9. package/dist/editor-x/editor.js.map +1 -1
  10. package/dist/index.cjs +772 -482
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.css +1418 -1120
  13. package/dist/index.css.map +1 -1
  14. package/dist/index.d.cts +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +775 -485
  17. package/dist/index.js.map +1 -1
  18. package/package.json +86 -84
  19. package/src/components/lexical-editor.tsx +140 -123
  20. package/src/editor-x/editor.tsx +20 -5
  21. package/src/editor-x/plugins.tsx +385 -380
  22. package/src/nodes/list-with-color-node.tsx +160 -160
  23. package/src/plugins/autocomplete-plugin.tsx +2574 -2574
  24. package/src/plugins/context-menu-plugin.tsx +239 -9
  25. package/src/plugins/floating-text-format-plugin.tsx +84 -92
  26. package/src/plugins/images-plugin.tsx +4 -4
  27. package/src/plugins/list-color-plugin.tsx +178 -178
  28. package/src/plugins/tab-focus-plugin.tsx +66 -66
  29. package/src/plugins/table-column-resizer-plugin.tsx +329 -190
  30. package/src/plugins/toolbar/block-format/block-format-data.tsx +4 -0
  31. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -40
  32. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -74
  33. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -40
  34. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +118 -117
  35. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +37 -53
  36. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +8 -15
  37. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +2 -3
  38. package/src/plugins/toolbar/history-toolbar-plugin.tsx +2 -5
  39. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +15 -23
  40. package/src/themes/_mixins.scss +158 -10
  41. package/src/themes/_variables.scss +168 -0
  42. package/src/themes/core/_code.scss +59 -0
  43. package/src/themes/core/_images.scss +80 -0
  44. package/src/themes/core/_lists.scss +214 -0
  45. package/src/themes/core/_misc.scss +46 -0
  46. package/src/themes/core/_reset.scss +119 -0
  47. package/src/themes/core/_tables.scss +145 -0
  48. package/src/themes/core/_text.scss +35 -0
  49. package/src/themes/core/_typography.scss +73 -0
  50. package/src/themes/editor-theme.scss +9 -623
  51. package/src/themes/editor-theme.ts +118 -118
  52. package/src/themes/plugins/_auto-embed.scss +11 -0
  53. package/src/themes/plugins/_color-picker.scss +103 -0
  54. package/src/themes/plugins/_draggable-block.scss +32 -0
  55. package/src/themes/plugins/_floating-link-editor.scss +47 -0
  56. package/src/themes/plugins/_floating-toolbars.scss +61 -0
  57. package/src/themes/plugins/_image-resizer.scss +38 -0
  58. package/src/themes/plugins/_image.scss +57 -0
  59. package/src/themes/plugins/_layout.scss +39 -0
  60. package/src/themes/plugins/_list-color.scss +23 -0
  61. package/src/themes/plugins/_mentions.scss +21 -0
  62. package/src/themes/plugins/_menus-and-pickers.scss +153 -0
  63. package/src/themes/plugins/_table.scss +20 -0
  64. package/src/themes/plugins/_toolbar.scss +36 -0
  65. package/src/themes/plugins/_tree-view.scss +11 -0
  66. package/src/themes/plugins.scss +20 -1165
  67. package/src/themes/ui-components/_animations.scss +31 -0
  68. package/src/themes/ui-components/_backgrounds.scss +27 -0
  69. package/src/themes/ui-components/_borders.scss +20 -0
  70. package/src/themes/ui-components/_button.scss +176 -0
  71. package/src/themes/ui-components/_checkbox.scss +14 -0
  72. package/src/themes/ui-components/_cursors.scss +31 -0
  73. package/src/themes/ui-components/_dialog.scss +86 -0
  74. package/src/themes/ui-components/_display-sizing.scss +100 -0
  75. package/src/themes/ui-components/_flex.scss +124 -0
  76. package/src/themes/ui-components/_form-layout.scss +15 -0
  77. package/src/themes/ui-components/_icons.scss +23 -0
  78. package/src/themes/ui-components/_input.scss +86 -0
  79. package/src/themes/ui-components/_label.scss +19 -0
  80. package/src/themes/ui-components/_loader.scss +9 -0
  81. package/src/themes/ui-components/_margins-paddings.scss +45 -0
  82. package/src/themes/ui-components/_popover.scss +16 -0
  83. package/src/themes/ui-components/_positioning.scss +73 -0
  84. package/src/themes/ui-components/_rounded.scss +19 -0
  85. package/src/themes/ui-components/_scroll-area.scss +11 -0
  86. package/src/themes/ui-components/_select.scss +110 -0
  87. package/src/themes/ui-components/_separator.scss +19 -0
  88. package/src/themes/ui-components/_shadow.scss +15 -0
  89. package/src/themes/ui-components/_tabs.scss +46 -0
  90. package/src/themes/ui-components/_text-utilities.scss +48 -0
  91. package/src/themes/ui-components/_toggle-toolbar.scss +128 -0
  92. package/src/themes/ui-components/_toggle.scss +80 -0
  93. package/src/themes/ui-components/_typography.scss +22 -0
  94. package/src/themes/ui-components.scss +27 -937
  95. package/src/transformers/markdown-list-transformer.ts +51 -51
  96. package/src/ui/button.tsx +11 -2
  97. package/src/ui/collapsible.tsx +1 -1
  98. package/src/ui/dialog.tsx +2 -2
  99. package/src/ui/flex.tsx +4 -4
  100. package/src/ui/popover.tsx +1 -1
  101. package/src/ui/tooltip.tsx +2 -2
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
- import { useEffect, useRef } from "react"
3
+ import { useEffect, useRef, useState, useMemo, useLayoutEffect, useCallback } from "react"
4
+ import { createPortal } from "react-dom"
4
5
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
6
  import { useLexicalEditable } from "@lexical/react/useLexicalEditable"
6
7
  import {
@@ -18,10 +19,16 @@ type DragState = {
18
19
  columnIndex: number
19
20
  startX: number
20
21
  startWidth: number
21
- initialColWidths: number[]
22
+ initialColWidths: readonly number[]
22
23
  }
23
24
 
24
- const EDGE_HITBOX_PX = 12
25
+ type HoverState = {
26
+ cellKey: NodeKey
27
+ tableKey: NodeKey
28
+ edge: ResizeEdge
29
+ }
30
+
31
+ const EDGE_HITBOX_PX = 8
25
32
 
26
33
  /** Độ rộng tối thiểu khi kéo cột (px) — nhỏ hơn để có thể thu hẹp cột như "TT" */
27
34
  const MIN_COLUMN_WIDTH_PX = 15
@@ -46,7 +53,7 @@ function getResizeEdge(clientX: number, rect: DOMRect): ResizeEdge | null {
46
53
  return nearLeft ? "left" : "right"
47
54
  }
48
55
 
49
- function getColumnIndexFromTableMap(
56
+ function $getColumnIndexFromTableMap(
50
57
  tableNode: TableNode,
51
58
  tableCellNode: ReturnType<typeof $getNearestNodeFromDOMNode>
52
59
  ): number | null {
@@ -68,242 +75,374 @@ function getColumnIndexFromTableMap(
68
75
  return null
69
76
  }
70
77
 
71
- export function TableColumnResizerPlugin() {
78
+ export function TableColumnResizerPlugin({
79
+ anchorElem = document.body,
80
+ }: {
81
+ anchorElem?: HTMLElement
82
+ }) {
72
83
  const [editor] = useLexicalComposerContext()
73
84
  const isEditable = useLexicalEditable()
74
85
  const dragRef = useRef<DragState | null>(null)
75
-
76
- useEffect(() => {
77
- if (!isEditable) return
78
-
79
- const rootElement = editor.getRootElement()
80
- if (!rootElement) return
81
-
82
- const setCursor = (cursor: string) => {
83
- rootElement.style.cursor = cursor
84
- }
85
-
86
- const clearCursor = () => {
87
- setCursor("")
86
+ const [hoverState, setHoverState] = useState<HoverState | null>(null)
87
+ const resizerRef = useRef<HTMLDivElement | null>(null)
88
+
89
+ const onPointerDownImpl = useCallback((
90
+ event: PointerEvent,
91
+ cell: HTMLTableCellElement,
92
+ edge: ResizeEdge
93
+ ) => {
94
+ if (event.button !== 0) return
95
+
96
+ event.preventDefault()
97
+
98
+ // Capture pointer to handle dragging outside the resizer handle
99
+ const target = event.target as HTMLElement
100
+ if (target) {
101
+ target.setPointerCapture(event.pointerId)
88
102
  }
89
103
 
90
- const onPointerMove = (event: PointerEvent) => {
91
- if (dragRef.current) return
92
-
93
- const cell = getCellTarget(event.target)
94
- if (!cell) {
95
- clearCursor()
96
- return
97
- }
98
-
99
- const edge = getResizeEdge(event.clientX, cell.getBoundingClientRect())
100
-
101
- // Disable dragging left edge of the first column (visual check)
102
- if (edge === "left") {
103
- const table = cell.closest("table")
104
- if (table) {
105
- const tableRect = table.getBoundingClientRect()
106
- const cellRect = cell.getBoundingClientRect()
107
- // If cell left edge is close to table left edge (within tolerance)
108
- if (Math.abs(cellRect.left - tableRect.left) < 5) {
109
- clearCursor()
110
- return
111
- }
112
- }
113
- }
114
-
115
- setCursor(edge ? "col-resize" : "")
116
- }
117
-
118
- const onPointerDown = (event: PointerEvent) => {
119
- if (event.button !== 0) return
120
-
121
- const cell = getCellTarget(event.target)
122
- if (!cell) return
104
+ let nextDragState: DragState | null = null
123
105
 
124
- const edge = getResizeEdge(event.clientX, cell.getBoundingClientRect())
125
- if (!edge) return
106
+ editor.update(() => {
107
+ const tableCellNode = $getNearestNodeFromDOMNode(cell)
108
+ if (!$isTableCellNode(tableCellNode)) return
126
109
 
127
- event.preventDefault()
110
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode)
111
+ const tableElement = editor.getElementByKey(tableNode.getKey())
112
+ if (!tableElement) return
128
113
 
129
- let nextDragState: DragState | null = null
114
+ const tableMap = $computeTableMapSkipCellCheck(tableNode, null, null)[0]
115
+ const colCount = tableMap[0]?.length ?? 0
116
+ const columnIndexFromMap = $getColumnIndexFromTableMap(tableNode, tableCellNode)
130
117
 
131
- editor.read(() => {
132
- const tableCellNode = $getNearestNodeFromDOMNode(cell)
133
- if (!$isTableCellNode(tableCellNode)) return
118
+ if (columnIndexFromMap === null) return
134
119
 
135
- const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode)
136
- const columnIndexFromMap = getColumnIndexFromTableMap(tableNode, tableCellNode)
137
- if (columnIndexFromMap == null) return
120
+ const colSpan = tableCellNode.getColSpan()
121
+ const resizeColumnIndex =
122
+ edge === "left" ? columnIndexFromMap - 1 : columnIndexFromMap + colSpan - 1
138
123
 
139
- // Disable resizing left edge of the first column
140
- if (edge === "left" && columnIndexFromMap === 0) return
124
+ if (resizeColumnIndex < 0 || resizeColumnIndex >= colCount - 1) return
141
125
 
142
- const colSpan = tableCellNode.getColSpan()
143
- const resizeColumnIndex =
144
- edge === "left" ? columnIndexFromMap - 1 : columnIndexFromMap + colSpan - 1
126
+ let currentWidths = tableNode.getColWidths()
145
127
 
146
- let currentWidths = tableNode.getColWidths()
128
+ if (!currentWidths) {
129
+ // If no widths are stored in the node, measure the actual table
130
+ const columns = tableElement.querySelectorAll("col")
131
+ const widths: number[] = []
147
132
 
148
- if (!currentWidths) {
149
- const tableElement = editor.getElementByKey(tableNode.getKey())
150
- if (tableElement instanceof HTMLTableElement) {
151
- // Attempt to reconstruct widths from TableMap (handles merged cells better)
152
- const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null)
153
- const colCount = tableNode.getColumnCount()
154
- const widths: (number | undefined)[] = new Array(colCount).fill(undefined)
155
-
156
- for (let r = 0; r < tableMap.length; r++) {
157
- const row = tableMap[r]
158
- if (!row) continue
159
- for (let c = 0; c < row.length; c++) {
160
- if (widths[c] !== undefined) continue
161
-
162
- const tableMapCell = row[c]
163
- if (!tableMapCell) continue
164
-
165
- const { cell } = tableMapCell
166
- // Only measure if this cell spans 1 column (accurate width)
167
- if (cell.getColSpan() === 1) {
168
- const cellElem = editor.getElementByKey(cell.getKey())
169
- if (cellElem) {
170
- widths[c] = Math.round(cellElem.getBoundingClientRect().width)
171
- }
172
- }
173
- }
174
- // If all widths found, break early
175
- if (widths.every(w => w !== undefined)) break
133
+ if (columns.length === colCount) {
134
+ columns.forEach((col) => {
135
+ const styleWidth = (col as HTMLElement).style.width
136
+ if (styleWidth && styleWidth.endsWith("px")) {
137
+ widths.push(parseFloat(styleWidth))
138
+ } else {
139
+ const rect = col.getBoundingClientRect()
140
+ widths.push(rect.width || 0)
176
141
  }
142
+ })
143
+ }
177
144
 
178
- // Fallback: If still missing widths, try row 0 or distribute evenly
179
- if (widths.some(w => w === undefined)) {
180
- if (tableElement.rows.length > 0) {
181
- const row = tableElement.rows[0]
182
- if (row && row.cells.length === colCount) {
183
- // Row 0 is perfect match, use it for missing values or override?
184
- // Prefer row 0 if it's clean
185
- const rowWidths = Array.from(row.cells).map(c => Math.round(c.getBoundingClientRect().width))
186
- for (let i = 0; i < colCount; i++) {
187
- if (widths[i] === undefined) widths[i] = rowWidths[i]
188
- }
189
- }
190
- }
191
- }
192
-
193
- // Final fallback: distribute evenly for any remaining undefined
194
- if (widths.some(w => w === undefined)) {
195
- const tableWidth = tableElement.getBoundingClientRect().width
196
- const defaultWidth = tableWidth / colCount
197
- for (let i = 0; i < colCount; i++) {
198
- if (widths[i] === undefined) widths[i] = defaultWidth
199
- }
200
- }
201
-
202
- currentWidths = widths as number[]
145
+ // If columns don't match or are missing, try to measure cells in the first row
146
+ if (widths.length !== colCount) {
147
+ const firstRow = tableElement.querySelector("tr")
148
+ if (firstRow) {
149
+ const cells = firstRow.querySelectorAll("th, td")
150
+ // This is only accurate if there are no colSpans in the first row
151
+ if (cells.length === colCount) {
152
+ cells.forEach((cell) => {
153
+ widths.push(cell.getBoundingClientRect().width)
154
+ })
155
+ }
203
156
  }
204
157
  }
205
158
 
206
- if (!currentWidths) {
207
- currentWidths = Array(tableNode.getColumnCount()).fill(MIN_COLUMN_WIDTH_PX)
159
+ // Fallback to average width if still missing
160
+ if (widths.length !== colCount) {
161
+ const tableRect = tableElement.getBoundingClientRect()
162
+ const avgWidth = tableRect.width / (colCount || 1)
163
+ for (let i = 0; i < colCount; i++) {
164
+ widths.push(avgWidth)
165
+ }
208
166
  }
167
+
168
+ currentWidths = widths
169
+ }
209
170
 
210
- const startWidth =
211
- currentWidths[resizeColumnIndex] ??
212
- Math.max(Math.round(cell.getBoundingClientRect().width), MIN_COLUMN_WIDTH_PX)
171
+ if (currentWidths[resizeColumnIndex] === undefined || currentWidths[resizeColumnIndex + 1] === undefined) {
172
+ return
173
+ }
213
174
 
214
- nextDragState = {
215
- tableKey: tableNode.getKey(),
216
- columnIndex: resizeColumnIndex,
217
- startX: event.clientX,
218
- startWidth,
219
- initialColWidths: [...currentWidths],
220
- }
221
- })
175
+ nextDragState = {
176
+ columnIndex: resizeColumnIndex,
177
+ initialColWidths: currentWidths,
178
+ startX: event.clientX,
179
+ startWidth: currentWidths[resizeColumnIndex],
180
+ tableKey: tableNode.getKey(),
181
+ }
182
+ })
222
183
 
223
- if (!nextDragState) return
184
+ if (!nextDragState) return
224
185
 
225
- event.preventDefault()
226
- event.stopPropagation()
227
- dragRef.current = nextDragState
228
- setCursor("col-resize")
186
+ dragRef.current = nextDragState
187
+
188
+ // Prevent text selection while dragging
189
+ const rootElement = editor.getRootElement()
190
+ if (rootElement) {
191
+ rootElement.style.userSelect = "none"
192
+ rootElement.style.cursor = "col-resize"
229
193
  }
230
194
 
231
195
  const onPointerMoveDocument = (event: PointerEvent) => {
232
- const drag = dragRef.current
233
- if (!drag) return
196
+ const drag = dragRef.current
197
+ if (!drag) return
234
198
 
235
- const { initialColWidths, startX, columnIndex } = drag
236
- const deltaX = event.clientX - startX
199
+ event.preventDefault()
200
+ const deltaX = event.clientX - drag.startX
201
+ const { tableKey, columnIndex, initialColWidths } = drag
237
202
 
238
203
  editor.update(() => {
239
- const tableNode = $getNodeByKey(drag.tableKey)
204
+ const tableNode = $getNodeByKey(tableKey)
240
205
  if (!(tableNode instanceof TableNode)) return
241
206
 
242
- const colWidths = tableNode.getColWidths() ?? initialColWidths
207
+ const currentWidths = tableNode.getColWidths() || initialColWidths
208
+ const colCount = tableNode.getColumnCount()
243
209
  const nextColumnIndex = columnIndex + 1
244
- const hasNeighbor = nextColumnIndex < initialColWidths.length
245
-
246
- if (hasNeighbor) {
247
- // Strict "Borrow" Logic:
248
- // Adjust column width by borrowing from the neighbor.
249
- // The total width of the two columns remains constant, ensuring the table width doesn't change.
250
- const currentLeftWidth = initialColWidths[columnIndex]
251
- const currentRightWidth = initialColWidths[nextColumnIndex]
210
+
211
+ if (nextColumnIndex >= colCount) return
252
212
 
253
- if (currentLeftWidth === undefined || currentRightWidth === undefined) return
213
+ const currentLeftWidth = initialColWidths[columnIndex]
214
+ const currentRightWidth = initialColWidths[nextColumnIndex]
254
215
 
255
- const maxShrinkLeft = currentLeftWidth - MIN_COLUMN_WIDTH_PX
256
- const maxGrowLeft = currentRightWidth - MIN_COLUMN_WIDTH_PX
216
+ if (currentLeftWidth === undefined || currentRightWidth === undefined) return
257
217
 
258
- // Clamp deltaX to ensure neither column shrinks below MIN_COLUMN_WIDTH_PX
259
- const constrainedDelta = Math.min(Math.max(deltaX, -maxShrinkLeft), maxGrowLeft)
218
+ const maxShrinkLeft = currentLeftWidth - MIN_COLUMN_WIDTH_PX
219
+ const maxGrowLeft = currentRightWidth - MIN_COLUMN_WIDTH_PX
260
220
 
261
- const newLeftWidth = currentLeftWidth + constrainedDelta
262
- const newRightWidth = currentRightWidth - constrainedDelta
221
+ // Clamp deltaX to ensure neither column shrinks below MIN_COLUMN_WIDTH_PX
222
+ const constrainedDelta = Math.min(Math.max(deltaX, -maxShrinkLeft), maxGrowLeft)
263
223
 
264
- if (colWidths[columnIndex] === newLeftWidth && colWidths[nextColumnIndex] === newRightWidth)
265
- return
224
+ const newLeftWidth = currentLeftWidth + constrainedDelta
225
+ const newRightWidth = currentRightWidth - constrainedDelta
266
226
 
267
- const nextColWidths = [...colWidths]
268
- nextColWidths[columnIndex] = newLeftWidth
269
- nextColWidths[nextColumnIndex] = newRightWidth
270
- tableNode.setColWidths(nextColWidths)
227
+ // Create a clean copy of widths, filling with measured values if needed
228
+ const nextColWidths = Array.from({ length: colCount }, (_, i) => {
229
+ if (i === columnIndex) return newLeftWidth
230
+ if (i === nextColumnIndex) return newRightWidth
231
+ return currentWidths[i] ?? initialColWidths[i] ?? 0
232
+ })
233
+
234
+ // Use the correct method to update widths if setColWidths is not standard
235
+ if (typeof (tableNode as unknown as Record<string, unknown>).setColWidths === "function") {
236
+ ;((tableNode as unknown as Record<string, unknown>).setColWidths as (widths: number[]) => void)(nextColWidths)
271
237
  } else {
272
- // Fallback for last column (or if no neighbor exists)
273
- const currentWidth = initialColWidths[columnIndex]
274
- if (currentWidth === undefined) return
275
-
276
- const newWidth = Math.max(MIN_COLUMN_WIDTH_PX, Math.round(currentWidth + deltaX))
277
-
278
- if (colWidths[columnIndex] === newWidth) return
279
-
280
- const nextColWidths = [...colWidths]
281
- nextColWidths[columnIndex] = newWidth
282
- tableNode.setColWidths(nextColWidths)
238
+ // Fallback for different Lexical versions if needed
239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
+ ;(tableNode as any).__colWidths = nextColWidths
241
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
242
+ ;(tableNode as any).__widths = nextColWidths
243
+ tableNode.markDirty()
283
244
  }
284
245
  })
285
- }
246
+ }
247
+
248
+ const onPointerUpDocument = (event: PointerEvent) => {
249
+ const drag = dragRef.current
250
+ if (!drag) return
251
+
252
+ const target = event.target as HTMLElement
253
+ if (target && target.hasPointerCapture(event.pointerId)) {
254
+ target.releasePointerCapture(event.pointerId)
255
+ }
286
256
 
287
- const onPointerUpDocument = () => {
288
- if (!dragRef.current) return
289
257
  dragRef.current = null
290
- clearCursor()
258
+ const rootElement = editor.getRootElement()
259
+ if (rootElement) {
260
+ rootElement.style.userSelect = ""
261
+ rootElement.style.cursor = ""
262
+ }
263
+ document.removeEventListener("pointermove", onPointerMoveDocument)
264
+ document.removeEventListener("pointerup", onPointerUpDocument)
291
265
  }
292
266
 
293
- rootElement.addEventListener("pointermove", onPointerMove)
294
- rootElement.addEventListener("pointerdown", onPointerDown)
295
267
  document.addEventListener("pointermove", onPointerMoveDocument)
296
268
  document.addEventListener("pointerup", onPointerUpDocument)
269
+ }, [editor])
270
+
271
+ const updateResizerPosition = useCallback(() => {
272
+ const state = hoverState as HoverState | null
273
+ if (state === null || !resizerRef.current) return
274
+
275
+ const { cellKey, edge } = state
276
+ const cell = editor.getElementByKey(cellKey)
277
+ if (!cell) return
278
+
279
+ const cellRect = cell.getBoundingClientRect()
280
+ const anchorRect = anchorElem.getBoundingClientRect()
281
+
282
+ const resizerElem = resizerRef.current
283
+
284
+ // Find the table element to make the resizer span the entire table height
285
+ const tableElement = cell.closest("table")
286
+ let top = cellRect.top - anchorRect.top
287
+ let height = cellRect.height
288
+
289
+ if (tableElement) {
290
+ const tableRect = tableElement.getBoundingClientRect()
291
+ top = tableRect.top - anchorRect.top
292
+ height = tableRect.height
293
+ }
294
+
295
+ // Position at the edge, centered (width is 16px, so center is edge - 8)
296
+ const left = (edge === "left" ? cellRect.left : cellRect.right) - anchorRect.left - 8
297
+
298
+ resizerElem.style.transform = `translate(${left}px, ${top}px)`
299
+ resizerElem.style.height = `${height}px`
300
+ resizerElem.style.opacity = "1"
301
+ }, [editor, hoverState, anchorElem])
302
+
303
+ useLayoutEffect(() => {
304
+ updateResizerPosition()
305
+
306
+ // Update on window resize and scroll
307
+ window.addEventListener("resize", updateResizerPosition)
308
+ window.addEventListener("scroll", updateResizerPosition, true)
309
+
310
+ const updateListener = editor.registerUpdateListener(() => {
311
+ editor.read(() => {
312
+ const state = hoverState as HoverState | null
313
+ if (state !== null) {
314
+ const cell = editor.getElementByKey(state.cellKey)
315
+ if (!cell) {
316
+ setHoverState(null)
317
+ } else {
318
+ updateResizerPosition()
319
+ }
320
+ }
321
+ })
322
+ })
297
323
 
298
324
  return () => {
299
- rootElement.removeEventListener("pointermove", onPointerMove)
300
- rootElement.removeEventListener("pointerdown", onPointerDown)
301
- document.removeEventListener("pointermove", onPointerMoveDocument)
302
- document.removeEventListener("pointerup", onPointerUpDocument)
303
- clearCursor()
304
- dragRef.current = null
325
+ window.removeEventListener("resize", updateResizerPosition)
326
+ window.removeEventListener("scroll", updateResizerPosition, true)
327
+ updateListener()
305
328
  }
306
- }, [editor, isEditable])
329
+ }, [editor, hoverState, updateResizerPosition])
307
330
 
308
- return null
331
+ useEffect(() => {
332
+ if (!isEditable) return
333
+
334
+ const rootElement = editor.getRootElement()
335
+ if (!rootElement) return
336
+
337
+ const setCursor = (cursor: string) => {
338
+ if (rootElement) {
339
+ rootElement.style.cursor = cursor
340
+ }
341
+ }
342
+
343
+ const clearCursor = () => {
344
+ setCursor("")
345
+ }
346
+
347
+ const onPointerMove = (event: PointerEvent) => {
348
+ if (dragRef.current) return
349
+
350
+ // Do not show resizer while dragging/selecting (any button is pressed)
351
+ if (event.buttons !== 0) return
352
+
353
+ // If we are over the resizer, don't clear hover state
354
+ if (resizerRef.current && (event.target === resizerRef.current || resizerRef.current.contains(event.target as Node))) {
355
+ return
356
+ }
357
+
358
+ const cell = getCellTarget(event.target)
359
+
360
+ let nextHoverState: HoverState | null = null
361
+
362
+ if (cell) {
363
+ editor.read(() => {
364
+ const cellNode = $getNearestNodeFromDOMNode(cell)
365
+ if ($isTableCellNode(cellNode)) {
366
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(cellNode)
367
+ const cellKey = cellNode.getKey()
368
+ const tableKey = tableNode.getKey()
369
+
370
+ const edge = getResizeEdge(event.clientX, cell.getBoundingClientRect())
371
+ if (edge) {
372
+ const colCount = tableNode.getColumnCount()
373
+ const colSpan = cellNode.getColSpan()
374
+ const columnIndexFromMap = $getColumnIndexFromTableMap(tableNode, cellNode)
375
+
376
+ if (
377
+ columnIndexFromMap !== null &&
378
+ !((edge === "left" && columnIndexFromMap === 0) ||
379
+ (edge === "right" && columnIndexFromMap + colSpan === colCount))
380
+ ) {
381
+ nextHoverState = { cellKey, tableKey, edge }
382
+ }
383
+ }
384
+ }
385
+ })
386
+ }
387
+
388
+ if (!nextHoverState) {
389
+ if (hoverState !== null) {
390
+ clearCursor()
391
+ setHoverState(null)
392
+ }
393
+ } else {
394
+ setCursor("col-resize")
395
+ // @ts-expect-error hoverState might be null initially
396
+ if (!hoverState || hoverState.cellKey !== nextHoverState.cellKey || hoverState.edge !== nextHoverState.edge) {
397
+ setHoverState(nextHoverState)
398
+ }
399
+ }
400
+ }
401
+
402
+ anchorElem.addEventListener("pointermove", onPointerMove)
403
+
404
+ return () => {
405
+ anchorElem.removeEventListener("pointermove", onPointerMove)
406
+ clearCursor()
407
+ }
408
+ }, [editor, isEditable, hoverState, anchorElem])
409
+
410
+ const resizer = useMemo(() => {
411
+ const state = hoverState as HoverState | null
412
+ if (state === null) return null
413
+
414
+ const { cellKey, edge } = state
415
+
416
+ return createPortal(
417
+ <div
418
+ ref={resizerRef}
419
+ className="editor-table-cell-resizer"
420
+ style={{
421
+ position: "absolute",
422
+ top: 0,
423
+ left: 0,
424
+ width: "16px",
425
+ opacity: 1,
426
+ willChange: "transform",
427
+ userSelect: "none",
428
+ }}
429
+ onPointerDown={(event) => {
430
+ event.stopPropagation()
431
+ event.preventDefault()
432
+ const cell = editor.getElementByKey(cellKey)
433
+ if (cell instanceof HTMLTableCellElement) {
434
+ onPointerDownImpl(event.nativeEvent, cell, edge)
435
+ }
436
+ }}
437
+ >
438
+ <div
439
+ className="editor-table-cell-resize-ruler"
440
+ style={{ pointerEvents: "none" }}
441
+ />
442
+ </div>,
443
+ anchorElem
444
+ )
445
+ }, [hoverState, anchorElem, editor, onPointerDownImpl])
446
+
447
+ return resizer
309
448
  }
@@ -42,6 +42,10 @@ export const blockTypeToBlockName: Record<
42
42
  label: "Alpha List (a, b, c)",
43
43
  icon: <IconSize size="sm"><CaseSensitiveIcon /></IconSize>,
44
44
  },
45
+ "number-multi-level": {
46
+ label: "Multi-level List (1.1 / 1.1.1)",
47
+ icon: <IconSize size="sm"><ListOrderedIcon /></IconSize>,
48
+ },
45
49
  bullet: {
46
50
  label: "Bulleted List",
47
51
  icon: <IconSize size="sm"><ListIcon /></IconSize>,