| 1 | "use client"; |
| 2 | |
| 3 | import { |
| 4 | DndPlugin, |
| 5 | useDropLine, |
| 6 | type DragItemNode, |
| 7 | type ElementDragItemNode, |
| 8 | } from "@platejs/dnd"; |
| 9 | import { |
| 10 | BlockSelectionPlugin, |
| 11 | useBlockSelected, |
| 12 | } from "@platejs/selection/react"; |
| 13 | import { isSelectingCell } from "@platejs/table"; |
| 14 | import { |
| 15 | TablePlugin, |
| 16 | TableProvider, |
| 17 | useTableCellElement, |
| 18 | useTableCellElementResizable, |
| 19 | useTableElement, |
| 20 | } from "@platejs/table/react"; |
| 21 | import { GripVertical, Plus } from "lucide-react"; |
| 22 | import { |
| 23 | ElementApi, |
| 24 | KEYS, |
| 25 | PathApi, |
| 26 | type Path, |
| 27 | type TTableCellElement, |
| 28 | type TTableElement, |
| 29 | type TTableRowElement, |
| 30 | } from "platejs"; |
| 31 | import { |
| 32 | PlateElement, |
| 33 | useEditorPlugin, |
| 34 | useEditorRef, |
| 35 | useEditorSelector, |
| 36 | useElement, |
| 37 | useElementSelector, |
| 38 | usePluginOption, |
| 39 | useReadOnly, |
| 40 | withHOC, |
| 41 | type PlateEditor, |
| 42 | type PlateElementProps, |
| 43 | } from "platejs/react"; |
| 44 | import * as React from "react"; |
| 45 | import { type DropTargetMonitor } from "react-dnd"; |
| 46 | |
| 47 | import { Button } from "@/components/plate/ui/button"; |
| 48 | import { ResizeHandle } from "@/components/plate/ui/resize-handle"; |
| 49 | import { cn } from "@/lib/utils"; |
| 50 | import { blockSelectionVariants } from "../../../../plate/ui/block-selection"; |
| 51 | import { PresentationElement } from "../custom-elements/presentation-element"; |
| 52 | import { useDraggable } from "../dnd/hooks/useDraggable"; |
| 53 | |
| 54 | const TABLE_ROW_DRAG_TYPE = "presentation-table-row"; |
| 55 | |
| 56 | function setRefValue<T>(ref: React.Ref<T> | undefined, value: T | null) { |
| 57 | if (typeof ref === "function") { |
| 58 | ref(value); |
| 59 | return; |
| 60 | } |
| 61 | |
| 62 | if (ref) { |
| 63 | ref.current = value; |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | function isElementDragItem( |
| 68 | dragItem: DragItemNode, |
| 69 | ): dragItem is ElementDragItemNode { |
| 70 | return "element" in dragItem && "id" in dragItem; |
| 71 | } |
| 72 | |
| 73 | function getRowDropDirection( |
| 74 | monitor: DropTargetMonitor<DragItemNode, unknown>, |
| 75 | row: HTMLTableRowElement | null, |
| 76 | ) { |
| 77 | if (!row) return; |
| 78 | |
| 79 | const clientOffset = monitor.getClientOffset(); |
| 80 | if (!clientOffset) return; |
| 81 | |
| 82 | const rect = row.getBoundingClientRect(); |
| 83 | const hoverClientY = clientOffset.y - rect.top; |
| 84 | |
| 85 | return hoverClientY < rect.height / 2 ? "top" : "bottom"; |
| 86 | } |
| 87 | |
| 88 | function canDropTableRow( |
| 89 | editor: PlateEditor, |
| 90 | dragItem: DragItemNode, |
| 91 | dropElement: TTableRowElement, |
| 92 | ) { |
| 93 | if (!isElementDragItem(dragItem)) return false; |
| 94 | if (dragItem.element === dropElement) return false; |
| 95 | |
| 96 | const dragPath = editor.api.findPath(dragItem.element); |
| 97 | const dropPath = editor.api.findPath(dropElement); |
| 98 | |
| 99 | return Boolean( |
| 100 | dragPath && |
| 101 | dropPath && |
| 102 | PathApi.equals(PathApi.parent(dragPath), PathApi.parent(dropPath)), |
| 103 | ); |
| 104 | } |
| 105 | |
| 106 | function updateRowDropLine( |
| 107 | editor: PlateEditor, |
| 108 | element: TTableRowElement, |
| 109 | direction: "bottom" | "top" | undefined, |
| 110 | ) { |
| 111 | const { dropTarget } = editor.getOptions(DndPlugin); |
| 112 | const currentId = dropTarget?.id ?? null; |
| 113 | const currentLine = dropTarget?.line ?? ""; |
| 114 | const nextId = direction ? (element.id as string) : null; |
| 115 | const nextLine = direction ?? ""; |
| 116 | |
| 117 | if (currentId !== nextId || currentLine !== nextLine) { |
| 118 | editor.setOption(DndPlugin, "dropTarget", { |
| 119 | id: nextId, |
| 120 | line: nextLine, |
| 121 | }); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | function moveTableRow( |
| 126 | editor: PlateEditor, |
| 127 | dragItem: DragItemNode, |
| 128 | dropElement: TTableRowElement, |
| 129 | direction: "bottom" | "top", |
| 130 | ) { |
| 131 | if (!isElementDragItem(dragItem)) return false; |
| 132 | |
| 133 | const dragPath = editor.api.findPath(dragItem.element); |
| 134 | const hoveredPath = editor.api.findPath(dropElement); |
| 135 | |
| 136 | if (!dragPath || !hoveredPath) return false; |
| 137 | if (!PathApi.equals(PathApi.parent(dragPath), PathApi.parent(hoveredPath))) { |
| 138 | return false; |
| 139 | } |
| 140 | |
| 141 | if (direction === "bottom") { |
| 142 | if (PathApi.equals(dragPath, PathApi.next(hoveredPath))) return true; |
| 143 | |
| 144 | const to = |
| 145 | PathApi.isBefore(dragPath, hoveredPath) && |
| 146 | PathApi.isSibling(dragPath, hoveredPath) |
| 147 | ? hoveredPath |
| 148 | : PathApi.next(hoveredPath); |
| 149 | |
| 150 | editor.tf.moveNodes({ at: dragPath, to }); |
| 151 | return true; |
| 152 | } |
| 153 | |
| 154 | const previousPath = PathApi.previous(hoveredPath); |
| 155 | if (previousPath && PathApi.equals(dragPath, previousPath)) return true; |
| 156 | |
| 157 | const beforePath = [ |
| 158 | ...hoveredPath.slice(0, -1), |
| 159 | (hoveredPath.at(-1) ?? 0) - 1, |
| 160 | ]; |
| 161 | const to = |
| 162 | PathApi.isBefore(dragPath, beforePath) && |
| 163 | PathApi.isSibling(dragPath, beforePath) |
| 164 | ? beforePath |
| 165 | : hoveredPath; |
| 166 | |
| 167 | editor.tf.moveNodes({ at: dragPath, to }); |
| 168 | return true; |
| 169 | } |
| 170 | |
| 171 | function getLastTableRowPath( |
| 172 | tableElement: TTableElement, |
| 173 | tablePath: Path, |
| 174 | ): Path | undefined { |
| 175 | if (tableElement.children.length === 0) return undefined; |
| 176 | |
| 177 | return [...tablePath, tableElement.children.length - 1]; |
| 178 | } |
| 179 | |
| 180 | function getLastTableCellPath( |
| 181 | tableElement: TTableElement, |
| 182 | tablePath: Path, |
| 183 | ): Path | undefined { |
| 184 | for ( |
| 185 | let rowIndex = tableElement.children.length - 1; |
| 186 | rowIndex >= 0; |
| 187 | rowIndex -= 1 |
| 188 | ) { |
| 189 | const row = tableElement.children[rowIndex]; |
| 190 | |
| 191 | if (!ElementApi.isElement(row)) continue; |
| 192 | |
| 193 | if (row.children.length > 0) { |
| 194 | return [...tablePath, rowIndex, row.children.length - 1]; |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | return undefined; |
| 199 | } |
| 200 | |
| 201 | const PresentationTableElementWithProvider = withHOC( |
| 202 | TableProvider, |
| 203 | function PresentationTableElement({ |
| 204 | children, |
| 205 | ...props |
| 206 | }: PlateElementProps<TTableElement>) { |
| 207 | const editor = useEditorRef(); |
| 208 | const { tf } = useEditorPlugin(TablePlugin); |
| 209 | const readOnly = useReadOnly(); |
| 210 | const isSelectionAreaVisible = usePluginOption( |
| 211 | BlockSelectionPlugin, |
| 212 | "isSelectionAreaVisible", |
| 213 | ); |
| 214 | const hasControls = !readOnly && !isSelectionAreaVisible; |
| 215 | const { marginLeft, props: tableProps } = useTableElement(); |
| 216 | const isSelectingCells = isSelectingCell(props.editor); |
| 217 | const tableId = props.element.id; |
| 218 | const isTableBlockSelected = useBlockSelected( |
| 219 | typeof tableId === "string" ? tableId : undefined, |
| 220 | ); |
| 221 | const isSelectionInCurrentTable = useEditorSelector( |
| 222 | (currentEditor) => { |
| 223 | const tableEntry = currentEditor.api.above({ |
| 224 | match: { type: KEYS.table }, |
| 225 | }); |
| 226 | const [selectedTable] = tableEntry ?? []; |
| 227 | |
| 228 | return typeof tableId === "string" && selectedTable?.id === tableId; |
| 229 | }, |
| 230 | [tableId], |
| 231 | ); |
| 232 | const showTableGrowthControls = |
| 233 | hasControls && (isTableBlockSelected || isSelectionInCurrentTable); |
| 234 | |
| 235 | const colSizes = props.element.colSizes ?? []; |
| 236 | |
| 237 | const insertColumnAfter = React.useCallback(() => { |
| 238 | const tablePath = editor.api.findPath(props.element); |
| 239 | if (!tablePath) return; |
| 240 | |
| 241 | const fromCell = getLastTableCellPath(props.element, tablePath); |
| 242 | if (!fromCell) return; |
| 243 | |
| 244 | tf.insert.tableColumn({ fromCell, select: true }); |
| 245 | editor.tf.focus(); |
| 246 | }, [editor, props.element, tf]); |
| 247 | |
| 248 | const insertRowAfter = React.useCallback(() => { |
| 249 | const tablePath = editor.api.findPath(props.element); |
| 250 | if (!tablePath) return; |
| 251 | |
| 252 | const fromRow = getLastTableRowPath(props.element, tablePath); |
| 253 | if (!fromRow) return; |
| 254 | |
| 255 | tf.insert.tableRow({ fromRow, select: true }); |
| 256 | editor.tf.focus(); |
| 257 | }, [editor, props.element, tf]); |
| 258 | |
| 259 | const content = ( |
| 260 | <PresentationElement |
| 261 | {...props} |
| 262 | className={cn( |
| 263 | "overflow-x-auto py-5", |
| 264 | hasControls && "-ml-2 data-[slot=block-selection]:*:left-2", |
| 265 | )} |
| 266 | style={{ paddingLeft: marginLeft }} |
| 267 | > |
| 268 | <div |
| 269 | className={cn( |
| 270 | "group/table relative w-full bg-transparent", |
| 271 | showTableGrowthControls && |
| 272 | "grid grid-cols-[minmax(0,1fr)_1.25rem] grid-rows-[auto_1.25rem]", |
| 273 | )} |
| 274 | > |
| 275 | {isTableBlockSelected && ( |
| 276 | <div className={blockSelectionVariants()} contentEditable={false} /> |
| 277 | )} |
| 278 | <table |
| 279 | className={cn( |
| 280 | "mr-0 ml-px table h-px max-w-full table-fixed border-collapse bg-transparent text-(--presentation-text)", |
| 281 | isSelectingCells && "selection:bg-transparent", |
| 282 | colSizes && colSizes.length > 0 && colSizes?.every((s) => s !== 0) |
| 283 | ? "w-fit" |
| 284 | : "w-full", |
| 285 | )} |
| 286 | {...tableProps} |
| 287 | > |
| 288 | <tbody className="w-full">{children}</tbody> |
| 289 | </table> |
| 290 | {showTableGrowthControls && ( |
| 291 | <> |
| 292 | <Button |
| 293 | aria-label="Add column" |
| 294 | className={cn( |
| 295 | "z-40 h-full min-h-10 w-5 rounded-md border-border/80 bg-muted/80 p-0 text-muted-foreground shadow-xs backdrop-blur-sm", |
| 296 | "transition-colors duration-100 hover:bg-background hover:text-foreground", |
| 297 | )} |
| 298 | contentEditable={false} |
| 299 | onClick={insertColumnAfter} |
| 300 | onMouseDown={(event) => event.preventDefault()} |
| 301 | size="icon" |
| 302 | title="Add column" |
| 303 | type="button" |
| 304 | variant="outline" |
| 305 | > |
| 306 | <Plus className="size-3.5" data-ppt-ignore="true" /> |
| 307 | </Button> |
| 308 | <Button |
| 309 | aria-label="Add row" |
| 310 | className={cn( |
| 311 | "z-40 col-span-2 h-5 w-auto rounded-md border-border/80 bg-muted/80 p-0 text-muted-foreground shadow-xs backdrop-blur-sm", |
| 312 | "transition-colors duration-100 hover:bg-background hover:text-foreground", |
| 313 | )} |
| 314 | contentEditable={false} |
| 315 | onClick={insertRowAfter} |
| 316 | onMouseDown={(event) => event.preventDefault()} |
| 317 | size="icon" |
| 318 | title="Add row" |
| 319 | type="button" |
| 320 | variant="outline" |
| 321 | > |
| 322 | <Plus className="size-3.5" data-ppt-ignore="true" /> |
| 323 | </Button> |
| 324 | </> |
| 325 | )} |
| 326 | </div> |
| 327 | </PresentationElement> |
| 328 | ); |
| 329 | |
| 330 | return content; |
| 331 | }, |
| 332 | ); |
| 333 | |
| 334 | export function PresentationTableElement( |
| 335 | props: PlateElementProps<TTableElement>, |
| 336 | ) { |
| 337 | return <PresentationTableElementWithProvider {...props} />; |
| 338 | } |
| 339 | |
| 340 | export function PresentationTableRowElement( |
| 341 | props: PlateElementProps<TTableRowElement>, |
| 342 | ) { |
| 343 | const { element } = props; |
| 344 | const readOnly = useReadOnly(); |
| 345 | const editor = useEditorRef(); |
| 346 | const isSelectionAreaVisible = usePluginOption( |
| 347 | BlockSelectionPlugin, |
| 348 | "isSelectionAreaVisible", |
| 349 | ); |
| 350 | const hasControls = !readOnly && !isSelectionAreaVisible; |
| 351 | |
| 352 | const { isDragging, nodeRef, handleRef } = useDraggable<HTMLTableRowElement>({ |
| 353 | element, |
| 354 | type: TABLE_ROW_DRAG_TYPE, |
| 355 | drag: { |
| 356 | disableFreeformDrag: true, |
| 357 | }, |
| 358 | preview: { disable: true }, |
| 359 | drop: { |
| 360 | hover: (dragItem, monitor) => { |
| 361 | const direction = getRowDropDirection(monitor, nodeRef.current); |
| 362 | |
| 363 | if (!direction || !canDropTableRow(editor, dragItem, element)) { |
| 364 | updateRowDropLine(editor, element, undefined); |
| 365 | return; |
| 366 | } |
| 367 | |
| 368 | updateRowDropLine(editor, element, direction); |
| 369 | }, |
| 370 | }, |
| 371 | onDropHandler: (_, { dragItem, monitor }) => { |
| 372 | const direction = getRowDropDirection(monitor, nodeRef.current); |
| 373 | if (!direction) return true; |
| 374 | |
| 375 | const moved = moveTableRow(editor, dragItem, element, direction); |
| 376 | if (moved) editor.tf.focus(); |
| 377 | |
| 378 | updateRowDropLine(editor, element, undefined); |
| 379 | return true; |
| 380 | }, |
| 381 | }); |
| 382 | const rowRef = React.useCallback( |
| 383 | (node: HTMLTableRowElement | null) => { |
| 384 | setRefValue<HTMLTableRowElement>(props.ref, node); |
| 385 | setRefValue<HTMLTableRowElement>(nodeRef, node); |
| 386 | }, |
| 387 | [nodeRef, props.ref], |
| 388 | ); |
| 389 | |
| 390 | return ( |
| 391 | <PlateElement |
| 392 | {...props} |
| 393 | ref={rowRef} |
| 394 | as="tr" |
| 395 | className={cn("group/row", isDragging && "opacity-50")} |
| 396 | > |
| 397 | {hasControls && ( |
| 398 | <td className="w-2 select-none" contentEditable={false}> |
| 399 | <RowDragHandle dragRef={handleRef} /> |
| 400 | <RowDropLine /> |
| 401 | </td> |
| 402 | )} |
| 403 | |
| 404 | {props.children} |
| 405 | </PlateElement> |
| 406 | ); |
| 407 | } |
| 408 | |
| 409 | function RowDragHandle({ dragRef }: { dragRef: React.Ref<HTMLButtonElement> }) { |
| 410 | const editor = useEditorRef(); |
| 411 | const element = useElement(); |
| 412 | |
| 413 | return ( |
| 414 | <button |
| 415 | ref={dragRef} |
| 416 | type="button" |
| 417 | className={cn( |
| 418 | "absolute top-1/2 z-51 w-4 -translate-y-1/2 p-0.5 focus-visible:ring-0 focus-visible:ring-offset-0", |
| 419 | "cursor-grab active:cursor-grabbing", |
| 420 | 'flex items-center rounded bg-accent opacity-0 outline transition-opacity duration-100 group-hover/row:opacity-100 group-has-data-[resizing="true"]/row:opacity-0', |
| 421 | )} |
| 422 | onClick={() => { |
| 423 | editor.tf.select(element); |
| 424 | }} |
| 425 | > |
| 426 | <GripVertical |
| 427 | className="size-3.5 text-muted-foreground" |
| 428 | data-ppt-ignore="true" |
| 429 | /> |
| 430 | </button> |
| 431 | ); |
| 432 | } |
| 433 | |
| 434 | function RowDropLine() { |
| 435 | const { dropLine } = useDropLine(); |
| 436 | |
| 437 | if (!dropLine) return null; |
| 438 | |
| 439 | return ( |
| 440 | <div |
| 441 | className={cn( |
| 442 | "absolute inset-x-0 left-2 z-50 h-0.5 rounded-full bg-brand/50", |
| 443 | dropLine === "top" ? "-top-px" : "-bottom-px", |
| 444 | )} |
| 445 | /> |
| 446 | ); |
| 447 | } |
| 448 | |
| 449 | export function PresentationTableCellElement({ |
| 450 | isHeader, |
| 451 | ...props |
| 452 | }: PlateElementProps<TTableCellElement> & { |
| 453 | isHeader?: boolean; |
| 454 | }) { |
| 455 | const { api } = useEditorPlugin(TablePlugin); |
| 456 | const readOnly = useReadOnly(); |
| 457 | const element = props.element; |
| 458 | |
| 459 | const rowId = useElementSelector(([node]) => node.id as string, [], { |
| 460 | key: KEYS.tr, |
| 461 | }); |
| 462 | const isSelectingRow = useBlockSelected(rowId); |
| 463 | const isSelectionAreaVisible = usePluginOption( |
| 464 | BlockSelectionPlugin, |
| 465 | "isSelectionAreaVisible", |
| 466 | ); |
| 467 | const selectedCellIds = usePluginOption(TablePlugin, "selectedCellIds"); |
| 468 | |
| 469 | const { borders, colIndex, colSpan, minHeight, rowIndex, selected, width } = |
| 470 | useTableCellElement(); |
| 471 | const isCellSelected = |
| 472 | selected || |
| 473 | (typeof element.id === "string" && |
| 474 | (selectedCellIds?.includes(element.id) ?? false)); |
| 475 | |
| 476 | const { bottomProps, hiddenLeft, leftProps, rightProps } = |
| 477 | useTableCellElementResizable({ |
| 478 | colIndex, |
| 479 | colSpan, |
| 480 | rowIndex, |
| 481 | }); |
| 482 | |
| 483 | return ( |
| 484 | <PlateElement |
| 485 | {...props} |
| 486 | as={isHeader ? "th" : "td"} |
| 487 | className={cn( |
| 488 | "relative h-full overflow-visible border-none bg-transparent p-0", |
| 489 | isHeader && "text-left *:m-0", |
| 490 | "before:inset-0 before:z-10 before:size-full", |
| 491 | (isCellSelected || isSelectingRow) && "before:bg-brand/20", |
| 492 | "before:absolute before:box-border before:content-[''] before:select-none", |
| 493 | borders.bottom?.size && `before:border-b before:border-b-border`, |
| 494 | borders.right?.size && `before:border-r before:border-r-border`, |
| 495 | borders.left?.size && `before:border-l before:border-l-border`, |
| 496 | borders.top?.size && `before:border-t before:border-t-border`, |
| 497 | )} |
| 498 | style={ |
| 499 | { |
| 500 | "--cellBackground": element.background, |
| 501 | maxWidth: width || 240, |
| 502 | width: width || undefined, |
| 503 | minWidth: width || 120, |
| 504 | backgroundColor: |
| 505 | element.background ?? |
| 506 | (isHeader ? "var(--presentation-card-background)" : undefined), |
| 507 | } as React.CSSProperties |
| 508 | } |
| 509 | attributes={{ |
| 510 | ...props.attributes, |
| 511 | colSpan: api.table.getColSpan(element), |
| 512 | rowSpan: api.table.getRowSpan(element), |
| 513 | }} |
| 514 | > |
| 515 | {(isCellSelected || isSelectingRow) && ( |
| 516 | <div |
| 517 | className="pointer-events-none absolute inset-0 z-10 bg-brand/20" |
| 518 | contentEditable={false} |
| 519 | /> |
| 520 | )} |
| 521 | |
| 522 | <div |
| 523 | className={cn( |
| 524 | "relative z-20 box-border h-full rounded-md px-3 py-2", |
| 525 | isHeader ? "text-lg font-bold text-primary" : "presentation-text", |
| 526 | )} |
| 527 | style={{ minHeight }} |
| 528 | > |
| 529 | {props.children} |
| 530 | </div> |
| 531 | |
| 532 | {!isSelectionAreaVisible && ( |
| 533 | <div |
| 534 | className="group absolute top-0 size-full select-none" |
| 535 | contentEditable={false} |
| 536 | suppressContentEditableWarning={true} |
| 537 | > |
| 538 | {!readOnly && ( |
| 539 | <> |
| 540 | <ResizeHandle |
| 541 | {...rightProps} |
| 542 | className="-top-2 -right-1 h-[calc(100%+8px)] w-2" |
| 543 | data-col={colIndex} |
| 544 | /> |
| 545 | <ResizeHandle {...bottomProps} className="-bottom-1 h-2" /> |
| 546 | {!hiddenLeft && ( |
| 547 | <ResizeHandle |
| 548 | {...leftProps} |
| 549 | className="top-0 -left-1 w-2" |
| 550 | data-resizer-left={colIndex === 0 ? "true" : undefined} |
| 551 | /> |
| 552 | )} |
| 553 | |
| 554 | <div |
| 555 | className={cn( |
| 556 | "absolute top-0 z-30 hidden h-full w-1 bg-ring", |
| 557 | "right-[-1.5px]", |
| 558 | getColumnResizeClass(colIndex), |
| 559 | )} |
| 560 | /> |
| 561 | {colIndex === 0 && ( |
| 562 | <div |
| 563 | className={cn( |
| 564 | "absolute top-0 z-30 h-full w-1 bg-ring", |
| 565 | "left-[-1.5px]", |
| 566 | 'hidden animate-in fade-in group-has-[[data-resizer-left]:hover]/table:block group-has-[[data-resizer-left][data-resizing="true"]]/table:block', |
| 567 | )} |
| 568 | /> |
| 569 | )} |
| 570 | </> |
| 571 | )} |
| 572 | </div> |
| 573 | )} |
| 574 | |
| 575 | {isSelectingRow && ( |
| 576 | <div className={blockSelectionVariants()} contentEditable={false} /> |
| 577 | )} |
| 578 | </PlateElement> |
| 579 | ); |
| 580 | } |
| 581 | |
| 582 | export function PresentationTableCellHeaderElement( |
| 583 | props: React.ComponentProps<typeof PresentationTableCellElement>, |
| 584 | ) { |
| 585 | return <PresentationTableCellElement {...props} isHeader />; |
| 586 | } |
| 587 | |
| 588 | const COLUMN_RESIZE_CLASSES: Record<number, string> = { |
| 589 | 0: 'group-has-[[data-col="0"]:hover]/table:block group-has-[[data-col="0"][data-resizing="true"]]/table:block', |
| 590 | 1: 'group-has-[[data-col="1"]:hover]/table:block group-has-[[data-col="1"][data-resizing="true"]]/table:block', |
| 591 | 2: 'group-has-[[data-col="2"]:hover]/table:block group-has-[[data-col="2"][data-resizing="true"]]/table:block', |
| 592 | 3: 'group-has-[[data-col="3"]:hover]/table:block group-has-[[data-col="3"][data-resizing="true"]]/table:block', |
| 593 | 4: 'group-has-[[data-col="4"]:hover]/table:block group-has-[[data-col="4"][data-resizing="true"]]/table:block', |
| 594 | 5: 'group-has-[[data-col="5"]:hover]/table:block group-has-[[data-col="5"][data-resizing="true"]]/table:block', |
| 595 | 6: 'group-has-[[data-col="6"]:hover]/table:block group-has-[[data-col="6"][data-resizing="true"]]/table:block', |
| 596 | 7: 'group-has-[[data-col="7"]:hover]/table:block group-has-[[data-col="7"][data-resizing="true"]]/table:block', |
| 597 | 8: 'group-has-[[data-col="8"]:hover]/table:block group-has-[[data-col="8"][data-resizing="true"]]/table:block', |
| 598 | 9: 'group-has-[[data-col="9"]:hover]/table:block group-has-[[data-col="9"][data-resizing="true"]]/table:block', |
| 599 | 10: 'group-has-[[data-col="10"]:hover]/table:block group-has-[[data-col="10"][data-resizing="true"]]/table:block', |
| 600 | }; |
| 601 | |
| 602 | function getColumnResizeClass(colIndex: number) { |
| 603 | return COLUMN_RESIZE_CLASSES[colIndex] ?? ""; |
| 604 | } |
| 605 |