Skip to content

Commit

Permalink
Merge pull request #4012 from udecode/feat/dnd-row
Browse files Browse the repository at this point in the history
Table row dnd bis
  • Loading branch information
zbeyens authored Jan 21, 2025
2 parents a9e90f1 + 10128e6 commit eef056f
Show file tree
Hide file tree
Showing 33 changed files with 155 additions and 178 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-eels-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@udecode/plate-alignment': patch
---

Deprecate useAlignDropdownMenuState, useAlignDropdownMenu
12 changes: 12 additions & 0 deletions .changeset/tidy-spies-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@udecode/plate-autoformat': patch
'@udecode/plate-suggestion': patch
'@udecode/plate-selection': patch
'@udecode/plate-combobox': patch
'@udecode/plate-table': patch
'@udecode/plate-link': patch
'@udecode/plate-dnd': patch
'@udecode/plate-ai': patch
---

Fix overrideEditor insertText missing options
40 changes: 0 additions & 40 deletions apps/www/content/docs/en/alignment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,43 +71,3 @@ Options for the `setNodes` function.
</APISubList>
</APIItem>
</APIParameters>

## API Components

### useAlignDropdownMenuState

<APIReturns>
<APIItem name="value" type="'left' | 'center' | 'right' | 'justify'">
The alignment value.
</APIItem>
</APIReturns>

### useAlignDropdownMenu

<APIState>
<APIItem name="value" type="'left' | 'center' | 'right' | 'justify'">
The alignment value.
</APIItem>
</APIState>

<APIReturns>
<APIItem name="radioGroupProps" type="object">
Props for the radio group.
<APISubList>
<APISubListItem
parent="radioGroupProps"
name="value"
type="'left' | 'center' | 'right' | 'justify'"
>
The alignment value.
</APISubListItem>
<APISubListItem
parent="radioGroupProps"
name="onValueChange"
type="function"
>
Callback to set the alignment value.
</APISubListItem>
</APISubList>
</APIItem>
</APIReturns>
5 changes: 3 additions & 2 deletions apps/www/content/docs/en/components/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver

### January 21 #18.7

- `table-row-element`: support row dnd
- `table-element`, `table-row-element`: support row dnd and selection
- `plate-element`: add `blockSelectionClassName` prop
- `editor`: z-50 for selection area
- `draggable`:
- Replace `editor.api.blockSelection.replaceSelectedIds` with `editor.api.blockSelection.clear`
Expand All @@ -24,7 +25,7 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver
- Replace `editor.api.blockSelection.addSelectedRow` with `editor.api.blockSelection.set`:
- `ai-menu`
- `equation-popover`

- `align-dropdown-menu`: deprecate


### January 18 #18.6
Expand Down
4 changes: 2 additions & 2 deletions apps/www/content/docs/en/migration/slate-to-plate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ const MyPlugin = createPlatePlugin({
key: 'myPlugin',
}).overrideEditor(({ editor, tf: { insertText } }) => ({
transforms: {
insertText(text) {
insertText(text, options) {
// Custom logic
insertText(text);
insertText(text, options);
},
}
}));
Expand Down
4 changes: 2 additions & 2 deletions apps/www/content/docs/en/plugin-methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ const MyPlugin = createPlatePlugin({
key: 'myPlugin',
}).overrideEditor(({ editor, tf: { insertText }, api: { isInline } }) => ({
transforms: {
insertText(text) {
insertText(text, options) {
// Override insertText behavior
insertText(text);
insertText(text, options);
},
},
api: {
Expand Down
2 changes: 2 additions & 0 deletions apps/www/content/docs/en/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ docs:
- Grid cell selection.
- Cell selection expansion with `Shift+Arrow` keys.
- Copying and pasting cells.
- Row drag-and-drop reordering
- Row selection via drag handle

</PackageInfo>

Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/ai-menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"files": [
{
"content": "'use client';\n\nimport * as React from 'react';\n\nimport { type NodeEntry, isHotkey } from '@udecode/plate';\nimport { useEditorPlugin, useHotkeys } from '@udecode/plate/react';\nimport {\n AIChatPlugin,\n useEditorChat,\n useLastAssistantMessage,\n} from '@udecode/plate-ai/react';\nimport {\n BlockSelectionPlugin,\n useIsSelecting,\n} from '@udecode/plate-selection/react';\nimport { Loader2Icon } from 'lucide-react';\n\nimport { useChat } from '@/components/editor/use-chat';\n\nimport { AIChatEditor } from './ai-chat-editor';\nimport { AIMenuItems } from './ai-menu-items';\nimport { Command, CommandList, InputCommand } from './command';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\n\nexport function AIMenu() {\n const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);\n const open = useOption('open');\n const mode = useOption('mode');\n const isSelecting = useIsSelecting();\n\n const [value, setValue] = React.useState('');\n\n const chat = useChat();\n\n const { input, isLoading, messages, setInput } = chat;\n const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(\n null\n );\n\n const content = useLastAssistantMessage()?.content;\n\n const setOpen = (open: boolean) => {\n if (open) {\n api.aiChat.show();\n } else {\n api.aiChat.hide();\n }\n };\n\n const show = (anchorElement: HTMLElement) => {\n setAnchorElement(anchorElement);\n setOpen(true);\n };\n\n useEditorChat({\n chat,\n onOpenBlockSelection: (blocks: NodeEntry[]) => {\n show(editor.api.toDOMNode(blocks.at(-1)![0])!);\n },\n onOpenChange: (open) => {\n if (!open) {\n setAnchorElement(null);\n setInput('');\n }\n },\n onOpenCursor: () => {\n const [ancestor] = editor.api.block({ highest: true })!;\n\n if (!editor.api.isAt({ end: true }) && !editor.api.isEmpty(ancestor)) {\n editor\n .getApi(BlockSelectionPlugin)\n .blockSelection.addSelectedRow(ancestor.id as string);\n }\n\n show(editor.api.toDOMNode(ancestor)!);\n },\n onOpenSelection: () => {\n show(editor.api.toDOMNode(editor.api.blocks().at(-1)![0])!);\n },\n });\n\n useHotkeys(\n 'meta+j',\n () => {\n api.aiChat.show();\n },\n { enableOnContentEditable: true, enableOnFormTags: true }\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen} modal={false}>\n <PopoverAnchor virtualRef={{ current: anchorElement }} />\n\n <PopoverContent\n className=\"border-none bg-transparent p-0 shadow-none\"\n style={{\n width: anchorElement?.offsetWidth,\n }}\n onEscapeKeyDown={(e) => {\n e.preventDefault();\n\n if (isLoading) {\n api.aiChat.stop();\n } else {\n api.aiChat.hide();\n }\n }}\n align=\"center\"\n // avoidCollisions={false}\n side=\"bottom\"\n >\n <Command\n className=\"w-full rounded-lg border shadow-md\"\n value={value}\n onValueChange={setValue}\n >\n {mode === 'chat' && isSelecting && content && (\n <AIChatEditor content={content} />\n )}\n\n {isLoading ? (\n <div className=\"flex grow select-none items-center gap-2 p-2 text-sm text-muted-foreground\">\n <Loader2Icon className=\"size-4 animate-spin\" />\n {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n </div>\n ) : (\n <InputCommand\n variant=\"ghost\"\n className=\"rounded-none border-b border-solid border-border [&_svg]:hidden\"\n value={input}\n onKeyDown={(e) => {\n if (isHotkey('backspace')(e) && input.length === 0) {\n e.preventDefault();\n api.aiChat.hide();\n }\n if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n e.preventDefault();\n void api.aiChat.submit();\n }\n }}\n onValueChange={setInput}\n placeholder=\"Ask AI anything...\"\n data-plate-focus\n autoFocus\n />\n )}\n\n {!isLoading && (\n <CommandList>\n <AIMenuItems setValue={setValue} />\n </CommandList>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n );\n}\n",
"content": "'use client';\n\nimport * as React from 'react';\n\nimport { type NodeEntry, isHotkey } from '@udecode/plate';\nimport { useEditorPlugin, useHotkeys } from '@udecode/plate/react';\nimport {\n AIChatPlugin,\n useEditorChat,\n useLastAssistantMessage,\n} from '@udecode/plate-ai/react';\nimport {\n BlockSelectionPlugin,\n useIsSelecting,\n} from '@udecode/plate-selection/react';\nimport { Loader2Icon } from 'lucide-react';\n\nimport { useChat } from '@/components/editor/use-chat';\n\nimport { AIChatEditor } from './ai-chat-editor';\nimport { AIMenuItems } from './ai-menu-items';\nimport { Command, CommandList, InputCommand } from './command';\nimport { Popover, PopoverAnchor, PopoverContent } from './popover';\n\nexport function AIMenu() {\n const { api, editor, useOption } = useEditorPlugin(AIChatPlugin);\n const open = useOption('open');\n const mode = useOption('mode');\n const isSelecting = useIsSelecting();\n\n const [value, setValue] = React.useState('');\n\n const chat = useChat();\n\n const { input, isLoading, messages, setInput } = chat;\n const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(\n null\n );\n\n const content = useLastAssistantMessage()?.content;\n\n const setOpen = (open: boolean) => {\n if (open) {\n api.aiChat.show();\n } else {\n api.aiChat.hide();\n }\n };\n\n const show = (anchorElement: HTMLElement) => {\n setAnchorElement(anchorElement);\n setOpen(true);\n };\n\n useEditorChat({\n chat,\n onOpenBlockSelection: (blocks: NodeEntry[]) => {\n show(editor.api.toDOMNode(blocks.at(-1)![0])!);\n },\n onOpenChange: (open) => {\n if (!open) {\n setAnchorElement(null);\n setInput('');\n }\n },\n onOpenCursor: () => {\n const [ancestor] = editor.api.block({ highest: true })!;\n\n if (!editor.api.isAt({ end: true }) && !editor.api.isEmpty(ancestor)) {\n editor\n .getApi(BlockSelectionPlugin)\n .blockSelection.set(ancestor.id as string);\n }\n\n show(editor.api.toDOMNode(ancestor)!);\n },\n onOpenSelection: () => {\n show(editor.api.toDOMNode(editor.api.blocks().at(-1)![0])!);\n },\n });\n\n useHotkeys(\n 'meta+j',\n () => {\n api.aiChat.show();\n },\n { enableOnContentEditable: true, enableOnFormTags: true }\n );\n\n return (\n <Popover open={open} onOpenChange={setOpen} modal={false}>\n <PopoverAnchor virtualRef={{ current: anchorElement }} />\n\n <PopoverContent\n className=\"border-none bg-transparent p-0 shadow-none\"\n style={{\n width: anchorElement?.offsetWidth,\n }}\n onEscapeKeyDown={(e) => {\n e.preventDefault();\n\n if (isLoading) {\n api.aiChat.stop();\n } else {\n api.aiChat.hide();\n }\n }}\n align=\"center\"\n // avoidCollisions={false}\n side=\"bottom\"\n >\n <Command\n className=\"w-full rounded-lg border shadow-md\"\n value={value}\n onValueChange={setValue}\n >\n {mode === 'chat' && isSelecting && content && (\n <AIChatEditor content={content} />\n )}\n\n {isLoading ? (\n <div className=\"flex grow select-none items-center gap-2 p-2 text-sm text-muted-foreground\">\n <Loader2Icon className=\"size-4 animate-spin\" />\n {messages.length > 1 ? 'Editing...' : 'Thinking...'}\n </div>\n ) : (\n <InputCommand\n variant=\"ghost\"\n className=\"rounded-none border-b border-solid border-border [&_svg]:hidden\"\n value={input}\n onKeyDown={(e) => {\n if (isHotkey('backspace')(e) && input.length === 0) {\n e.preventDefault();\n api.aiChat.hide();\n }\n if (isHotkey('enter')(e) && !e.shiftKey && !value) {\n e.preventDefault();\n void api.aiChat.submit();\n }\n }}\n onValueChange={setInput}\n placeholder=\"Ask AI anything...\"\n data-plate-focus\n autoFocus\n />\n )}\n\n {!isLoading && (\n <CommandList>\n <AIMenuItems setValue={setValue} />\n </CommandList>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n );\n}\n",
"path": "plate-ui/ai-menu.tsx",
"target": "components/plate-ui/ai-menu.tsx",
"type": "registry:ui"
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/align-dropdown-menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"files": [
{
"content": "'use client';\n\nimport React from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport {\n useAlignDropdownMenu,\n useAlignDropdownMenuState,\n} from '@udecode/plate-alignment/react';\nimport {\n AlignCenterIcon,\n AlignJustifyIcon,\n AlignLeftIcon,\n AlignRightIcon,\n} from 'lucide-react';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { ToolbarButton } from './toolbar';\n\nconst items = [\n {\n icon: AlignLeftIcon,\n value: 'left',\n },\n {\n icon: AlignCenterIcon,\n value: 'center',\n },\n {\n icon: AlignRightIcon,\n value: 'right',\n },\n {\n icon: AlignJustifyIcon,\n value: 'justify',\n },\n];\n\nexport function AlignDropdownMenu({ children, ...props }: DropdownMenuProps) {\n const state = useAlignDropdownMenuState();\n const { radioGroupProps } = useAlignDropdownMenu(state);\n\n const openState = useOpenState();\n const IconValue =\n items.find((item) => item.value === radioGroupProps.value)?.icon ??\n AlignLeftIcon;\n\n return (\n <DropdownMenu modal={false} {...openState} {...props}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton pressed={openState.open} tooltip=\"Align\" isDropdown>\n <IconValue />\n </ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent className=\"min-w-0\" align=\"start\">\n <DropdownMenuRadioGroup {...radioGroupProps}>\n {items.map(({ icon: Icon, value: itemValue }) => (\n <DropdownMenuRadioItem key={itemValue} value={itemValue} hideIcon>\n <Icon />\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n",
"content": "'use client';\n\nimport React from 'react';\n\nimport type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';\n\nimport { useEditorRef, useSelectionFragmentProp } from '@udecode/plate/react';\nimport { setAlign } from '@udecode/plate-alignment';\nimport {\n AlignCenterIcon,\n AlignJustifyIcon,\n AlignLeftIcon,\n AlignRightIcon,\n} from 'lucide-react';\n\nimport { STRUCTURAL_TYPES } from '@/components/editor/transforms';\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuTrigger,\n useOpenState,\n} from './dropdown-menu';\nimport { ToolbarButton } from './toolbar';\n\nconst items = [\n {\n icon: AlignLeftIcon,\n value: 'left',\n },\n {\n icon: AlignCenterIcon,\n value: 'center',\n },\n {\n icon: AlignRightIcon,\n value: 'right',\n },\n {\n icon: AlignJustifyIcon,\n value: 'justify',\n },\n];\n\nexport function AlignDropdownMenu({ children, ...props }: DropdownMenuProps) {\n const editor = useEditorRef();\n const value = useSelectionFragmentProp({\n defaultValue: 'start',\n getProp: (node) => node.align,\n structuralTypes: STRUCTURAL_TYPES,\n });\n\n const openState = useOpenState();\n const IconValue =\n items.find((item) => item.value === value)?.icon ?? AlignLeftIcon;\n\n return (\n <DropdownMenu modal={false} {...openState} {...props}>\n <DropdownMenuTrigger asChild>\n <ToolbarButton pressed={openState.open} tooltip=\"Align\" isDropdown>\n <IconValue />\n </ToolbarButton>\n </DropdownMenuTrigger>\n\n <DropdownMenuContent className=\"min-w-0\" align=\"start\">\n <DropdownMenuRadioGroup\n value={value}\n onValueChange={(value: any) => {\n setAlign(editor, { value: value });\n editor.tf.focus();\n }}\n >\n {items.map(({ icon: Icon, value: itemValue }) => (\n <DropdownMenuRadioItem key={itemValue} value={itemValue} hideIcon>\n <Icon />\n </DropdownMenuRadioItem>\n ))}\n </DropdownMenuRadioGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n",
"path": "plate-ui/align-dropdown-menu.tsx",
"target": "components/plate-ui/align-dropdown-menu.tsx",
"type": "registry:ui"
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/block-selection.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"files": [
{
"content": "'use client';\n\nimport React from 'react';\n\nimport { cn } from '@udecode/cn';\nimport { useBlockSelected } from '@udecode/plate-selection/react';\nimport { type VariantProps, cva } from 'class-variance-authority';\n\nexport const blockSelectionVariants = cva(\n 'pointer-events-none absolute inset-0 z-[1] bg-brand/[.13] transition-opacity',\n {\n defaultVariants: {\n active: true,\n },\n variants: {\n active: {\n false: 'opacity-0',\n true: 'opacity-100',\n },\n },\n }\n);\n\nexport function BlockSelection({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof blockSelectionVariants>) {\n const isBlockSelected = useBlockSelected();\n\n if (!isBlockSelected) return null;\n\n return (\n <div\n className={cn(\n blockSelectionVariants({\n active: isBlockSelected,\n }),\n className\n )}\n {...props}\n />\n );\n}\n",
"content": "'use client';\n\nimport React from 'react';\n\nimport { cn } from '@udecode/cn';\nimport { useEditorPlugin } from '@udecode/plate-core/react';\nimport { DndPlugin } from '@udecode/plate-dnd';\nimport { useBlockSelected } from '@udecode/plate-selection/react';\nimport { type VariantProps, cva } from 'class-variance-authority';\n\nexport const blockSelectionVariants = cva(\n 'pointer-events-none absolute inset-0 z-[1] bg-brand/[.13] transition-opacity',\n {\n defaultVariants: {\n active: true,\n },\n variants: {\n active: {\n false: 'opacity-0',\n true: 'opacity-100',\n },\n },\n }\n);\n\nexport function BlockSelection({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof blockSelectionVariants>) {\n const isBlockSelected = useBlockSelected();\n const { useOption } = useEditorPlugin(DndPlugin);\n const isDragging = useOption('isDragging');\n\n if (!isBlockSelected) return null;\n\n return (\n <div\n className={cn(\n blockSelectionVariants({\n active: isBlockSelected && !isDragging,\n }),\n className\n )}\n {...props}\n />\n );\n}\n",
"path": "plate-ui/block-selection.tsx",
"target": "components/plate-ui/block-selection.tsx",
"type": "registry:ui"
Expand Down
Loading

0 comments on commit eef056f

Please sign in to comment.