supermemory/apps/web/app/components/plate-ui/table-element.tsx
2025-01-20 17:50:45 -07:00

330 lines
10 KiB
TypeScript

'use client';
import React from 'react';
import type * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import type { TTableElement } from '@udecode/plate-table';
import { PopoverAnchor } from '@radix-ui/react-popover';
import { cn, withRef } from '@udecode/cn';
import { isSelectionExpanded } from '@udecode/plate-common';
import {
useEditorRef,
useEditorSelector,
useElement,
useRemoveNodeButton,
withHOC,
} from '@udecode/plate-common/react';
import {
TableProvider,
mergeTableCells,
unmergeTableCells,
useTableBordersDropdownMenuContentState,
useTableElement,
useTableElementState,
useTableMergeState,
} from '@udecode/plate-table/react';
import { type LucideProps, Combine, Trash2Icon, Ungroup } from 'lucide-react';
import { useReadOnly, useSelected } from 'slate-react';
import { Button } from './button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuTrigger,
} from './dropdown-menu';
import { PlateElement } from './plate-element';
import { Popover, PopoverContent, popoverVariants } from './popover';
export const TableBordersDropdownMenuContent = withRef<
typeof DropdownMenuPrimitive.Content
>((props, ref) => {
const {
getOnSelectTableBorder,
hasBottomBorder,
hasLeftBorder,
hasNoBorders,
hasOuterBorders,
hasRightBorder,
hasTopBorder,
} = useTableBordersDropdownMenuContentState();
return (
<DropdownMenuContent
ref={ref}
className={cn('min-w-[220px]')}
align="start"
side="right"
sideOffset={0}
{...props}
>
<DropdownMenuGroup>
<DropdownMenuCheckboxItem
checked={hasBottomBorder}
onCheckedChange={getOnSelectTableBorder('bottom')}
>
<BorderBottom />
<div>Bottom Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={hasTopBorder}
onCheckedChange={getOnSelectTableBorder('top')}
>
<BorderTop />
<div>Top Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={hasLeftBorder}
onCheckedChange={getOnSelectTableBorder('left')}
>
<BorderLeft />
<div>Left Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={hasRightBorder}
onCheckedChange={getOnSelectTableBorder('right')}
>
<BorderRight />
<div>Right Border</div>
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuCheckboxItem
checked={hasNoBorders}
onCheckedChange={getOnSelectTableBorder('none')}
>
<BorderNone />
<div>No Border</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={hasOuterBorders}
onCheckedChange={getOnSelectTableBorder('outer')}
>
<BorderAll />
<div>Outside Borders</div>
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
</DropdownMenuContent>
);
});
export const TableFloatingToolbar = withRef<typeof PopoverContent>(
({ children, ...props }, ref) => {
const element = useElement<TTableElement>();
const { props: buttonProps } = useRemoveNodeButton({ element });
const selectionCollapsed = useEditorSelector(
(editor) => !isSelectionExpanded(editor),
[]
);
const readOnly = useReadOnly();
const selected = useSelected();
const editor = useEditorRef();
const collapsed = !readOnly && selected && selectionCollapsed;
const open = !readOnly && selected;
const { canMerge, canUnmerge } = useTableMergeState();
const mergeContent = canMerge && (
<Button
variant="ghost"
onClick={() => mergeTableCells(editor)}
contentEditable={false}
isMenu
>
<Combine />
Merge
</Button>
);
const unmergeButton = canUnmerge && (
<Button
variant="ghost"
onClick={() => unmergeTableCells(editor)}
contentEditable={false}
isMenu
>
<Ungroup />
Unmerge
</Button>
);
const bordersContent = collapsed && (
<>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" isMenu>
<BorderAll />
Borders
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<TableBordersDropdownMenuContent />
</DropdownMenuPortal>
</DropdownMenu>
<Button variant="ghost" contentEditable={false} isMenu {...buttonProps}>
<Trash2Icon />
Delete
</Button>
</>
);
return (
<Popover open={open} modal={false}>
<PopoverAnchor asChild>{children}</PopoverAnchor>
{(canMerge || canUnmerge || collapsed) && (
<PopoverContent
ref={ref}
className={cn(popoverVariants(), 'flex w-[220px] flex-col p-1')}
onOpenAutoFocus={(e) => e.preventDefault()}
{...props}
>
{unmergeButton}
{mergeContent}
{bordersContent}
</PopoverContent>
)}
</Popover>
);
}
);
export const TableElement = withHOC(
TableProvider,
withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {
const { colSizes, isSelectingCell, marginLeft, minColumnWidth } =
useTableElementState();
const { colGroupProps, props: tableProps } = useTableElement();
return (
<TableFloatingToolbar>
<PlateElement
className={cn('overflow-x-auto', className)}
style={{ paddingLeft: marginLeft }}
{...props}
>
<table
ref={ref}
className={cn(
'my-4 ml-px mr-0 table h-px w-[calc(100%-6px)] table-fixed border-collapse',
isSelectingCell && '[&_*::selection]:bg-none'
)}
{...tableProps}
>
<colgroup {...colGroupProps}>
{colSizes.map((width, index) => (
<col
key={index}
style={{
minWidth: minColumnWidth,
width: width || undefined,
}}
/>
))}
</colgroup>
<tbody className="min-w-full">{children}</tbody>
</table>
</PlateElement>
</TableFloatingToolbar>
);
})
);
const BorderAll = (props: LucideProps) => (
<svg
fill="currentColor"
focusable="false"
height="48"
role="img"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6zm10 13h5a1 1 0 0 0 1-1v-5h-6v6zm-2-6H5v5a1 1 0 0 0 1 1h5v-6zm2-2h6V6a1 1 0 0 0-1-1h-5v6zm-2-6H6a1 1 0 0 0-1 1v5h6V5z" />
</svg>
);
const BorderBottom = (props: LucideProps) => (
<svg
fill="currentColor"
focusable="false"
height="48"
role="img"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M13 5a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zm-8 6a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm-2 7a1 1 0 1 1 2 0 1 1 0 0 0 1 1h12a1 1 0 0 0 1-1 1 1 0 1 1 2 0 3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm17-8a1 1 0 0 0-1 1v2a1 1 0 1 0 2 0v-2a1 1 0 0 0-1-1zM7 4a1 1 0 0 0-1-1 3 3 0 0 0-3 3 1 1 0 0 0 2 0 1 1 0 0 1 1-1 1 1 0 0 0 1-1zm11-1a1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3z" />
</svg>
);
const BorderLeft = (props: LucideProps) => (
<svg
fill="currentColor"
focusable="false"
height="48"
role="img"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M6 21a1 1 0 1 0 0-2 1 1 0 0 1-1-1V6a1 1 0 0 1 1-1 1 1 0 0 0 0-2 3 3 0 0 0-3 3v12a3 3 0 0 0 3 3zm7-16a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zm6 6a1 1 0 1 1 2 0v2a1 1 0 1 1-2 0v-2zm-5 9a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zm4-17a1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3zm-1 17a1 1 0 0 0 1 1 3 3 0 0 0 3-3 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 0 0-1 1z" />
</svg>
);
const BorderNone = (props: LucideProps) => (
<svg
fill="currentColor"
focusable="false"
height="48"
role="img"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M14 4a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zm-9 7a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm14 0a1 1 0 1 1 2 0v2a1 1 0 1 1-2 0v-2zm-6 10a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zM7 4a1 1 0 0 0-1-1 3 3 0 0 0-3 3 1 1 0 0 0 2 0 1 1 0 0 1 1-1 1 1 0 0 0 1-1zm11-1a1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3zM7 20a1 1 0 0 1-1 1 3 3 0 0 1-3-3 1 1 0 1 1 2 0 1 1 0 0 0 1 1 1 1 0 0 1 1 1zm11 1a1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 1 1 2 0 3 3 0 0 1-3 3z" />
</svg>
);
const BorderRight = (props: LucideProps) => (
<svg
fill="currentColor"
focusable="false"
height="48"
role="img"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M13 5a1 1 0 1 0 0-2h-2a1 1 0 1 0 0 2h2zm-8 6a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm9 9a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zM6 3a1 1 0 0 1 0 2 1 1 0 0 0-1 1 1 1 0 0 1-2 0 3 3 0 0 1 3-3zm1 17a1 1 0 0 1-1 1 3 3 0 0 1-3-3 1 1 0 1 1 2 0 1 1 0 0 0 1 1 1 1 0 0 1 1 1zm11 1a1 1 0 1 1 0-2 1 1 0 0 0 1-1V6a1 1 0 0 0-1-1 1 1 0 1 1 0-2 3 3 0 0 1 3 3v12a3 3 0 0 1-3 3z" />
</svg>
);
const BorderTop = (props: LucideProps) => (
<svg
fill="currentColor"
focusable="false"
height="48"
role="img"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M3 6a1 1 0 0 0 2 0 1 1 0 0 1 1-1h12a1 1 0 0 1 1 1 1 1 0 1 0 2 0 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3zm2 5a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2zm14 0a1 1 0 1 1 2 0v2a1 1 0 1 1-2 0v-2zm-5 9a1 1 0 0 1-1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1 1 1zm-8 1a1 1 0 1 0 0-2 1 1 0 0 1-1-1 1 1 0 1 0-2 0 3 3 0 0 0 3 3zm11-1a1 1 0 0 0 1 1 3 3 0 0 0 3-3 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 0 0-1 1z" />
</svg>
);