From 30b68123246ae49c3f582dcd45606a3d62c91dec Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 01:28:25 +0100 Subject: [PATCH 01/12] docs --- .../(app)/_components/installation-tab.tsx | 5 +- apps/www/src/config/customizer-items.ts | 20 +-- .../components/editor/use-create-editor.ts | 95 +++++----- .../components/editor/plugins/dnd-plugins.tsx | 5 + .../default/components/editor/transforms.ts | 6 +- .../editor/use-create-editor-list.ts | 93 +++++----- .../components/editor/use-create-editor.ts | 5 +- .../default/plate-ui/column-element.tsx | 24 +-- .../default/plate-ui/column-group-element.tsx | 55 ++++-- .../default/plate-ui/date-element.tsx | 9 +- .../registry/default/plate-ui/draggable.tsx | 165 +++++++++++++----- .../default/plate-ui/image-element.tsx | 15 +- .../default/plate-ui/media-video-element.tsx | 8 +- .../plate-ui/table-cell-element-static.tsx | 2 +- .../default/plate-ui/table-cell-element.tsx | 2 +- .../default/plate-ui/with-draggables.tsx | 148 ---------------- apps/www/src/registry/registry-ui.ts | 8 +- 17 files changed, 295 insertions(+), 370 deletions(-) delete mode 100644 apps/www/src/registry/default/plate-ui/with-draggables.tsx diff --git a/apps/www/src/app/(app)/_components/installation-tab.tsx b/apps/www/src/app/(app)/_components/installation-tab.tsx index f4e22a5853..be1bbd2137 100644 --- a/apps/www/src/app/(app)/_components/installation-tab.tsx +++ b/apps/www/src/app/(app)/_components/installation-tab.tsx @@ -309,7 +309,6 @@ export default function InstallationTab() { pluginsCode.push(formattedPlugin + ','); }); - const hasDraggable = components.some((comp) => comp.id === 'draggable'); const hasPlaceholder = components.some((comp) => comp.id === 'placeholder'); const usageCode = [ @@ -318,11 +317,11 @@ export default function InstallationTab() { pluginsCode.join('\n'), ' ],', ' override: {', - ` components: ${hasDraggable ? 'withDraggables(' : ''}${hasPlaceholder ? 'withPlaceholders(' : ''}({`, + ` components: ${hasPlaceholder ? 'withPlaceholders(' : ''}({`, ...componentsWithPluginKey.map( ({ pluginKey, usage }) => ` [${pluginKey}]: ${usage},` ), - ` })${hasPlaceholder ? ')' : ''}${hasDraggable ? ')' : ''},`, + ` })${hasPlaceholder ? ')' : ''},`, ' },', ' value: [', ' {', diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts index b1d23434a4..aaf77be80d 100644 --- a/apps/www/src/config/customizer-items.ts +++ b/apps/www/src/config/customizer-items.ts @@ -417,16 +417,16 @@ export const customizerItems: Record = { [DndPlugin.key]: { id: DndPlugin.key, badges: [customizerBadges.handler, customizerBadges.ui], - components: [ - { - id: 'draggable', - filename: 'with-draggables', - label: 'Draggable', - registry: 'draggable', - route: getComponentNavItem('draggable').href, - usage: 'withDraggables', - }, - ], + // components: [ + // { + // id: 'draggable', + // filename: 'with-draggables', + // label: 'Draggable', + // registry: 'draggable', + // route: getComponentNavItem('draggable').href, + // usage: 'withDraggables', + // }, + // ], customImports: [ `import { DndProvider } from 'react-dnd';`, `import { HTML5Backend } from 'react-dnd-html5-backend';`, diff --git a/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts b/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts index 95cf5f8276..441b42ddd1 100644 --- a/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts +++ b/apps/www/src/registry/default/block/editor-ai/components/editor/use-create-editor.ts @@ -92,59 +92,56 @@ import { TableElement } from '@/registry/default/plate-ui/table-element'; import { TableRowElement } from '@/registry/default/plate-ui/table-row-element'; import { TocElement } from '@/registry/default/plate-ui/toc-element'; import { ToggleElement } from '@/registry/default/plate-ui/toggle-element'; -import { withDraggables } from '@/registry/default/plate-ui/with-draggables'; export const useCreateEditor = () => { return usePlateEditor({ override: { - components: withDraggables( - withPlaceholders({ - [AIPlugin.key]: AILeaf, - [AudioPlugin.key]: MediaAudioElement, - [BlockquotePlugin.key]: BlockquoteElement, - [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }), - [CodeBlockPlugin.key]: CodeBlockElement, - [CodeLinePlugin.key]: CodeLineElement, - [CodePlugin.key]: CodeLeaf, - [CodeSyntaxPlugin.key]: CodeSyntaxLeaf, - [ColumnItemPlugin.key]: ColumnElement, - [ColumnPlugin.key]: ColumnGroupElement, - [CommentsPlugin.key]: CommentLeaf, - [DatePlugin.key]: DateElement, - [EmojiInputPlugin.key]: EmojiInputElement, - [ExcalidrawPlugin.key]: ExcalidrawElement, - [FilePlugin.key]: MediaFileElement, - [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), - [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), - [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), - [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), - [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), - [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), - [HighlightPlugin.key]: HighlightLeaf, - [HorizontalRulePlugin.key]: HrElement, - [ImagePlugin.key]: ImageElement, - [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), - [KbdPlugin.key]: KbdLeaf, - [LinkPlugin.key]: LinkElement, - [MediaEmbedPlugin.key]: MediaEmbedElement, - [MentionInputPlugin.key]: MentionInputElement, - [MentionPlugin.key]: MentionElement, - [ParagraphPlugin.key]: ParagraphElement, - [PlaceholderPlugin.key]: MediaPlaceholderElement, - [SlashInputPlugin.key]: SlashInputElement, - [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), - [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }), - [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }), - [TableCellHeaderPlugin.key]: TableCellHeaderElement, - [TableCellPlugin.key]: TableCellElement, - [TablePlugin.key]: TableElement, - [TableRowPlugin.key]: TableRowElement, - [TocPlugin.key]: TocElement, - [TogglePlugin.key]: ToggleElement, - [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }), - [VideoPlugin.key]: MediaVideoElement, - }) - ), + components: withPlaceholders({ + [AIPlugin.key]: AILeaf, + [AudioPlugin.key]: MediaAudioElement, + [BlockquotePlugin.key]: BlockquoteElement, + [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }), + [CodeBlockPlugin.key]: CodeBlockElement, + [CodeLinePlugin.key]: CodeLineElement, + [CodePlugin.key]: CodeLeaf, + [CodeSyntaxPlugin.key]: CodeSyntaxLeaf, + [ColumnItemPlugin.key]: ColumnElement, + [ColumnPlugin.key]: ColumnGroupElement, + [CommentsPlugin.key]: CommentLeaf, + [DatePlugin.key]: DateElement, + [EmojiInputPlugin.key]: EmojiInputElement, + [ExcalidrawPlugin.key]: ExcalidrawElement, + [FilePlugin.key]: MediaFileElement, + [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), + [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), + [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), + [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), + [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), + [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), + [HighlightPlugin.key]: HighlightLeaf, + [HorizontalRulePlugin.key]: HrElement, + [ImagePlugin.key]: ImageElement, + [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), + [KbdPlugin.key]: KbdLeaf, + [LinkPlugin.key]: LinkElement, + [MediaEmbedPlugin.key]: MediaEmbedElement, + [MentionInputPlugin.key]: MentionInputElement, + [MentionPlugin.key]: MentionElement, + [ParagraphPlugin.key]: ParagraphElement, + [PlaceholderPlugin.key]: MediaPlaceholderElement, + [SlashInputPlugin.key]: SlashInputElement, + [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), + [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }), + [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }), + [TableCellHeaderPlugin.key]: TableCellHeaderElement, + [TableCellPlugin.key]: TableCellElement, + [TablePlugin.key]: TableElement, + [TableRowPlugin.key]: TableRowElement, + [TocPlugin.key]: TocElement, + [TogglePlugin.key]: ToggleElement, + [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }), + [VideoPlugin.key]: MediaVideoElement, + }), }, plugins: [ ...copilotPlugins, diff --git a/apps/www/src/registry/default/components/editor/plugins/dnd-plugins.tsx b/apps/www/src/registry/default/components/editor/plugins/dnd-plugins.tsx index f5569c0f58..48a405835b 100644 --- a/apps/www/src/registry/default/components/editor/plugins/dnd-plugins.tsx +++ b/apps/www/src/registry/default/components/editor/plugins/dnd-plugins.tsx @@ -4,6 +4,8 @@ import { DndPlugin } from '@udecode/plate-dnd'; import { PlaceholderPlugin } from '@udecode/plate-media/react'; import { NodeIdPlugin } from '@udecode/plate-node-id'; +import { DraggableAboveNodes } from '@/registry/default/plate-ui/draggable'; + export const dndPlugins = [ NodeIdPlugin, DndPlugin.configure({ @@ -15,5 +17,8 @@ export const dndPlugins = [ .insert.media(dragItem.files, { at: target, nextBlock: false }); }, }, + render: { + aboveNodes: DraggableAboveNodes, + }, }), ] as const; diff --git a/apps/www/src/registry/default/components/editor/transforms.ts b/apps/www/src/registry/default/components/editor/transforms.ts index 26d7431155..bd1448eca8 100644 --- a/apps/www/src/registry/default/components/editor/transforms.ts +++ b/apps/www/src/registry/default/components/editor/transforms.ts @@ -53,7 +53,7 @@ import { } from '@udecode/plate-table/react'; import { Path } from 'slate'; -export const STRUCTURAL_TYPES = [ +export const STRUCTURAL_TYPES: string[] = [ ColumnPlugin.key, ColumnItemPlugin.key, TablePlugin.key, @@ -79,7 +79,7 @@ const insertBlockMap: Record< (editor: PlateEditor, type: string) => void > = { [ACTION_THREE_COLUMNS]: (editor) => - insertColumnGroup(editor, { layout: 3, select: true }), + insertColumnGroup(editor, { columns: 3, select: true }), [AudioPlugin.key]: (editor) => insertAudioPlaceholder(editor, { select: true }), [CalloutPlugin.key]: (editor) => insertCallout(editor, { select: true }), @@ -163,7 +163,7 @@ const setBlockMap: Record< string, (editor: PlateEditor, type: string, entry: TNodeEntry) => void > = { - [ACTION_THREE_COLUMNS]: (editor) => toggleColumnGroup(editor, { layout: 3 }), + [ACTION_THREE_COLUMNS]: (editor) => toggleColumnGroup(editor, { columns: 3 }), [INDENT_LIST_KEYS.todo]: setList, [ListStyleType.Decimal]: setList, [ListStyleType.Disc]: setList, diff --git a/apps/www/src/registry/default/components/editor/use-create-editor-list.ts b/apps/www/src/registry/default/components/editor/use-create-editor-list.ts index 1b90c14313..acacdacadc 100644 --- a/apps/www/src/registry/default/components/editor/use-create-editor-list.ts +++ b/apps/www/src/registry/default/components/editor/use-create-editor-list.ts @@ -88,58 +88,55 @@ import { TableElement } from '@/registry/default/plate-ui/table-element'; import { TableRowElement } from '@/registry/default/plate-ui/table-row-element'; import { TocElement } from '@/registry/default/plate-ui/toc-element'; import { ToggleElement } from '@/registry/default/plate-ui/toggle-element'; -import { withDraggables } from '@/registry/default/plate-ui/with-draggables'; export const useCreateEditor = () => { return usePlateEditor({ override: { - components: withDraggables( - withPlaceholders({ - [AIPlugin.key]: AILeaf, - [BlockquotePlugin.key]: BlockquoteElement, - [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }), - [BulletedListPlugin.key]: withProps(ListElement, { variant: 'ul' }), - [CodeBlockPlugin.key]: CodeBlockElement, - [CodeLinePlugin.key]: CodeLineElement, - [CodePlugin.key]: CodeLeaf, - [CodeSyntaxPlugin.key]: CodeSyntaxLeaf, - [ColumnItemPlugin.key]: ColumnElement, - [ColumnPlugin.key]: ColumnGroupElement, - [CommentsPlugin.key]: CommentLeaf, - [DatePlugin.key]: DateElement, - [EmojiInputPlugin.key]: EmojiInputElement, - [ExcalidrawPlugin.key]: ExcalidrawElement, - [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), - [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), - [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), - [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), - [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), - [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), - [HighlightPlugin.key]: HighlightLeaf, - [HorizontalRulePlugin.key]: HrElement, - [ImagePlugin.key]: ImageElement, - [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), - [KbdPlugin.key]: KbdLeaf, - [LinkPlugin.key]: LinkElement, - [ListItemPlugin.key]: withProps(PlateElement, { as: 'li' }), - [MediaEmbedPlugin.key]: MediaEmbedElement, - [MentionInputPlugin.key]: MentionInputElement, - [MentionPlugin.key]: MentionElement, - [NumberedListPlugin.key]: withProps(ListElement, { variant: 'ol' }), - [ParagraphPlugin.key]: ParagraphElement, - [SlashInputPlugin.key]: SlashInputElement, - [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), - [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }), - [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }), - [TableCellHeaderPlugin.key]: TableCellHeaderElement, - [TableCellPlugin.key]: TableCellElement, - [TablePlugin.key]: TableElement, - [TableRowPlugin.key]: TableRowElement, - [TocPlugin.key]: TocElement, - [TogglePlugin.key]: ToggleElement, - [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }), - }) - ), + components: withPlaceholders({ + [AIPlugin.key]: AILeaf, + [BlockquotePlugin.key]: BlockquoteElement, + [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }), + [BulletedListPlugin.key]: withProps(ListElement, { variant: 'ul' }), + [CodeBlockPlugin.key]: CodeBlockElement, + [CodeLinePlugin.key]: CodeLineElement, + [CodePlugin.key]: CodeLeaf, + [CodeSyntaxPlugin.key]: CodeSyntaxLeaf, + [ColumnItemPlugin.key]: ColumnElement, + [ColumnPlugin.key]: ColumnGroupElement, + [CommentsPlugin.key]: CommentLeaf, + [DatePlugin.key]: DateElement, + [EmojiInputPlugin.key]: EmojiInputElement, + [ExcalidrawPlugin.key]: ExcalidrawElement, + [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }), + [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }), + [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }), + [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }), + [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }), + [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }), + [HighlightPlugin.key]: HighlightLeaf, + [HorizontalRulePlugin.key]: HrElement, + [ImagePlugin.key]: ImageElement, + [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }), + [KbdPlugin.key]: KbdLeaf, + [LinkPlugin.key]: LinkElement, + [ListItemPlugin.key]: withProps(PlateElement, { as: 'li' }), + [MediaEmbedPlugin.key]: MediaEmbedElement, + [MentionInputPlugin.key]: MentionInputElement, + [MentionPlugin.key]: MentionElement, + [NumberedListPlugin.key]: withProps(ListElement, { variant: 'ol' }), + [ParagraphPlugin.key]: ParagraphElement, + [SlashInputPlugin.key]: SlashInputElement, + [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }), + [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }), + [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }), + [TableCellHeaderPlugin.key]: TableCellHeaderElement, + [TableCellPlugin.key]: TableCellElement, + [TablePlugin.key]: TableElement, + [TableRowPlugin.key]: TableRowElement, + [TocPlugin.key]: TocElement, + [TogglePlugin.key]: ToggleElement, + [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }), + }), }, plugins: [ ...copilotPlugins, diff --git a/apps/www/src/registry/default/components/editor/use-create-editor.ts b/apps/www/src/registry/default/components/editor/use-create-editor.ts index d452926a5c..f3820bcff5 100644 --- a/apps/www/src/registry/default/components/editor/use-create-editor.ts +++ b/apps/www/src/registry/default/components/editor/use-create-editor.ts @@ -91,7 +91,6 @@ import { TableElement } from '@/registry/default/plate-ui/table-element'; import { TableRowElement } from '@/registry/default/plate-ui/table-row-element'; import { TocElement } from '@/registry/default/plate-ui/toc-element'; import { ToggleElement } from '@/registry/default/plate-ui/toggle-element'; -import { withDraggables } from '@/registry/default/plate-ui/with-draggables'; import { editorPlugins, viewPlugins } from './plugins/editor-plugins'; @@ -162,9 +161,7 @@ export const useCreateEditor = ( { override: { components: { - ...(readOnly - ? viewComponents - : withPlaceholders(withDraggables(editorComponents))), + ...(readOnly ? viewComponents : withPlaceholders(editorComponents)), ...components, }, ...override, diff --git a/apps/www/src/registry/default/plate-ui/column-element.tsx b/apps/www/src/registry/default/plate-ui/column-element.tsx index 1e5caf7369..271aac256a 100644 --- a/apps/www/src/registry/default/plate-ui/column-element.tsx +++ b/apps/www/src/registry/default/plate-ui/column-element.tsx @@ -6,11 +6,7 @@ import type { TColumnElement } from '@udecode/plate-layout'; import { cn, useComposedRef, withRef } from '@udecode/cn'; import { useElement, withHOC } from '@udecode/plate-common/react'; -import { - useDraggable, - useDraggableState, - useDropLine, -} from '@udecode/plate-dnd'; +import { useDraggable, useDropLine } from '@udecode/plate-dnd'; import { ResizableProvider } from '@udecode/plate-resizable'; import { GripHorizontal } from 'lucide-react'; import { Path } from 'slate'; @@ -26,24 +22,20 @@ import { TooltipTrigger, } from './tooltip'; -const DRAG_ITEM_COLUMN = 'column'; - export const ColumnElement = withHOC( ResizableProvider, withRef(({ children, className, ...props }, ref) => { const readOnly = useReadOnly(); const { width } = useElement(); - const state = useDraggableState({ + const { isDragging, previewRef, handleRef } = useDraggable({ canDropNode: ({ dragEntry, dropEntry }) => Path.equals(Path.parent(dragEntry[1]), Path.parent(dropEntry[1])), element: props.element, orientation: 'horizontal', - type: DRAG_ITEM_COLUMN, + type: 'column', }); - const { previewRef, handleRef } = useDraggable(state); - return (
{children} @@ -108,9 +100,9 @@ const DropLine = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const state = useDropLine({ orientation: 'horizontal' }); + const { dropLine } = useDropLine({ orientation: 'horizontal' }); - if (!state.dropLine) return null; + if (!dropLine) return null; return (
( ); export function ColumnFloatingToolbar({ children }: React.PropsWithChildren) { + const editor = useEditorRef(); const readOnly = useReadOnly(); - const { - setDoubleColumn, - setDoubleSideDoubleColumn, - setLeftSideDoubleColumn, - setRightSideDoubleColumn, - setThreeColumn, - } = useColumnState(); - const element = useElement(ColumnItemPlugin.key); + const columnGroupElement = useElement(ColumnPlugin.key); const { props: buttonProps } = useRemoveNodeButton({ element }); const isOpen = useDebouncePopoverOpen(); + const onColumnChange = (widths: string[]) => { + setColumns(editor, { + at: findNodePath(editor, columnGroupElement), + widths, + }); + }; + if (readOnly) return <>{children}; return ( @@ -61,26 +70,38 @@ export function ColumnFloatingToolbar({ children }: React.PropsWithChildren) { sideOffset={10} >
- - - diff --git a/apps/www/src/registry/default/plate-ui/date-element.tsx b/apps/www/src/registry/default/plate-ui/date-element.tsx index f914ec68b7..f9f90708db 100644 --- a/apps/www/src/registry/default/plate-ui/date-element.tsx +++ b/apps/www/src/registry/default/plate-ui/date-element.tsx @@ -1,8 +1,7 @@ 'use client'; import { cn, withRef } from '@udecode/cn'; -import { setNodes } from '@udecode/plate-common'; -import { findPath } from '@udecode/plate-common/react'; +import { setNode } from '@udecode/plate-common/react'; import { useReadOnly } from 'slate-react'; import { Calendar } from './calendar'; @@ -74,11 +73,7 @@ export const DateElement = withRef( onSelect={(date) => { if (!date) return; - setNodes( - editor, - { date: date.toDateString() }, - { at: findPath(editor, element) } - ); + setNode(editor, element, { date: date.toDateString() }); }} mode="single" initialFocus diff --git a/apps/www/src/registry/default/plate-ui/draggable.tsx b/apps/www/src/registry/default/plate-ui/draggable.tsx index ec514ce65c..96951824bd 100644 --- a/apps/www/src/registry/default/plate-ui/draggable.tsx +++ b/apps/www/src/registry/default/plate-ui/draggable.tsx @@ -1,28 +1,41 @@ /* eslint-disable tailwindcss/no-custom-classname */ 'use client'; -import React from 'react'; - -import type { TEditor } from '@udecode/plate-common'; -import type { DropTargetMonitor } from 'react-dnd'; +import React, { useMemo } from 'react'; import { cn, withRef } from '@udecode/cn'; +import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; +import { CodeBlockPlugin } from '@udecode/plate-code-block/react'; +import { isType, someNode } from '@udecode/plate-common'; import { - type PlateElementProps, + type NodeWrapperComponent, + type PlateRenderElementProps, MemoizedChildren, + ParagraphPlugin, useEditorPlugin, useEditorRef, + useElement, } from '@udecode/plate-common/react'; +import { useDraggable, useDropLine } from '@udecode/plate-dnd'; +import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react'; +import { HEADING_KEYS } from '@udecode/plate-heading'; +import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react'; import { - type DragItemNode, - useDraggable, - useDraggableGutter, - useDraggableState, - useDropLine, -} from '@udecode/plate-dnd'; + ImagePlugin, + MediaEmbedPlugin, + PlaceholderPlugin, +} from '@udecode/plate-media/react'; import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; +import { + TableCellPlugin, + TablePlugin, + TableRowPlugin, +} from '@udecode/plate-table/react'; +import { TogglePlugin } from '@udecode/plate-toggle/react'; import { GripVertical } from 'lucide-react'; -import { useSelected } from 'slate-react'; +import { useReadOnly, useSelected } from 'slate-react'; + +import { STRUCTURAL_TYPES } from '@/registry/default/components/editor/transforms'; import { Tooltip, @@ -32,30 +45,58 @@ import { TooltipTrigger, } from './tooltip'; -export interface DraggableProps extends PlateElementProps { - /** - * Intercepts the drop handling. If `false` is returned, the default drop - * behavior is called after. If `true` is returned, the default behavior is - * not called. - */ - onDropHandler?: ( - editor: TEditor, - props: { - id: string; - dragItem: DragItemNode; - monitor: DropTargetMonitor; - nodeRef: any; +const UNDRAGGABLE_KEYS = [ + ColumnItemPlugin.key, + TableRowPlugin.key, + TableCellPlugin.key, +]; + +export const DraggableAboveNodes: NodeWrapperComponent = (props) => { + const { editor, element, path } = props; + const readOnly = useReadOnly(); + + const enabled = useMemo(() => { + if (readOnly) return false; + if (path.length === 1 && !isType(editor, element, UNDRAGGABLE_KEYS)) { + return true; } - ) => boolean; -} + if (path.length === 3 && !isType(editor, element, UNDRAGGABLE_KEYS)) { + const block = someNode(editor, { + at: path, + match: { + type: editor.getType(ColumnPlugin), + }, + }); + + if (block) { + return true; + } + } + if (path.length === 4 && !isType(editor, element, UNDRAGGABLE_KEYS)) { + const block = someNode(editor, { + at: path, + match: { + type: editor.getType(TablePlugin), + }, + }); + + if (block) { + return true; + } + } + + return false; + }, [editor, element, path, readOnly]); -export const Draggable = withRef<'div', DraggableProps>( - ({ className, onDropHandler, ...props }, ref) => { - const { children, element } = props; + if (!enabled) return; - const state = useDraggableState({ element, onDropHandler }); - const { isDragging } = state; - const { previewRef, handleRef } = useDraggable(state); + return (props) => ; +}; + +export const Draggable = withRef<'div', PlateRenderElementProps>( + ({ className, ...props }, ref) => { + const { children, editor, element } = props; + const { isDragging, previewRef, handleRef } = useDraggable({ element }); return (
( className={cn( 'relative', isDragging && 'opacity-50', - 'group', + STRUCTURAL_TYPES.includes(element.type) + ? 'group/structural' + : 'group', className )} > -
+
>(({ children, className, ...props }, ref) => { - const { useOption } = useEditorPlugin(BlockSelectionPlugin); + const { editor, useOption } = useEditorPlugin(BlockSelectionPlugin); + const element = useElement(); const isSelectionAreaVisible = useOption('isSelectionAreaVisible'); - const gutter = useDraggableGutter(); const selected = useSelected(); + const isNodeType = (keys: string[] | string) => isType(editor, element, keys); + return (
{children}
@@ -150,21 +228,20 @@ const DragHandle = React.memo(() => { const DropLine = React.memo( React.forwardRef>( ({ className, ...props }, ref) => { - const state = useDropLine(); + const { dropLine } = useDropLine(); - if (!state.dropLine) return null; + if (!dropLine) return null; return (
diff --git a/apps/www/src/registry/default/plate-ui/image-element.tsx b/apps/www/src/registry/default/plate-ui/image-element.tsx index e05ea38aa3..337ae132ae 100644 --- a/apps/www/src/registry/default/plate-ui/image-element.tsx +++ b/apps/www/src/registry/default/plate-ui/image-element.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { cn, withRef } from '@udecode/cn'; -import { useEditorRef, withHOC } from '@udecode/plate-common/react'; -import { useDraggable, useDraggableState } from '@udecode/plate-dnd'; +import { withHOC } from '@udecode/plate-common/react'; +import { useDraggable } from '@udecode/plate-dnd'; import { Image, ImagePlugin, useMediaState } from '@udecode/plate-media/react'; import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable'; @@ -21,18 +21,13 @@ export const ImageElement = withHOC( ResizableProvider, withRef( ({ children, className, nodeProps, ...props }, ref) => { - const editor = useEditorRef(); - const { align = 'center', focused, readOnly, selected } = useMediaState(); const width = useResizableStore().get.width(); - const state = editor.plugins.dnd - ? useDraggableState({ element: props.element }) - : ({} as any); - - const { isDragging } = state; - const { handleRef } = useDraggable(state); + const { isDragging, handleRef } = useDraggable({ + element: props.element, + }); return ( diff --git a/apps/www/src/registry/default/plate-ui/media-video-element.tsx b/apps/www/src/registry/default/plate-ui/media-video-element.tsx index 571da9a527..2140e048c2 100644 --- a/apps/www/src/registry/default/plate-ui/media-video-element.tsx +++ b/apps/www/src/registry/default/plate-ui/media-video-element.tsx @@ -6,7 +6,7 @@ import ReactPlayer from 'react-player'; import { cn, withRef } from '@udecode/cn'; import { useEditorMounted, withHOC } from '@udecode/plate-common/react'; -import { useDraggable, useDraggableState } from '@udecode/plate-dnd'; +import { useDraggable } from '@udecode/plate-dnd'; import { parseTwitterUrl, parseVideoUrl } from '@udecode/plate-media'; import { useMediaState } from '@udecode/plate-media/react'; import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable'; @@ -39,9 +39,9 @@ export const MediaVideoElement = withHOC( const isTweet = true; - const state = useDraggableState({ element: props.element }); - const { isDragging } = state; - const { handleRef } = useDraggable(state); + const { isDragging, handleRef } = useDraggable({ + element: props.element, + }); return ( -
+
{children}
diff --git a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx index b0bacd0987..abd0f7e7a3 100644 --- a/apps/www/src/registry/default/plate-ui/table-cell-element.tsx +++ b/apps/www/src/registry/default/plate-ui/table-cell-element.tsx @@ -86,7 +86,7 @@ export const TableCellElement = withRef< } >
> - > -) => - withDraggablePrimitive(Draggable, Component, options as any); - -export const withDraggablesPrimitive = createNodesWithHOC(withDraggable); - -export const withDraggables = (components: any) => { - return withDraggablesPrimitive(components, [ - { - keys: [ParagraphPlugin.key, 'ul', 'ol'], - level: 0, - }, - { - key: HEADING_KEYS.h1, - draggableProps: { - className: - '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-1 [&_.slate-gutterLeft]:text-[1.875em]', - }, - }, - { - key: HEADING_KEYS.h2, - draggableProps: { - className: - '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-1 [&_.slate-gutterLeft]:text-[1.5em]', - }, - }, - { - key: HEADING_KEYS.h3, - draggableProps: { - className: - '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:pt-[2px] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-1 [&_.slate-gutterLeft]:text-[1.25em]', - }, - }, - { - keys: [HEADING_KEYS.h4, HEADING_KEYS.h5], - draggableProps: { - className: - '[&_.slate-blockToolbarWrapper]:h-[1.3em] [&_.slate-gutterLeft]:pt-[3px] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0 [&_.slate-gutterLeft]:text-[1.1em]', - }, - }, - { - keys: [ParagraphPlugin.key], - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-[3px] [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - keys: [HEADING_KEYS.h6, 'ul', 'ol'], - draggableProps: { - className: '[&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: BlockquotePlugin.key, - draggableProps: { - className: '[&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: CodeBlockPlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-6 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: ImagePlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: MediaEmbedPlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: ExcalidrawPlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: TogglePlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: ColumnPlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-0 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: PlaceholderPlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-3 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - { - key: TablePlugin.key, - draggableProps: { - className: - '[&_.slate-gutterLeft]:pt-3 [&_.slate-gutterLeft]:px-0 [&_.slate-gutterLeft]:pb-0', - }, - }, - ]); -}; diff --git a/apps/www/src/registry/registry-ui.ts b/apps/www/src/registry/registry-ui.ts index edbbbbd9b1..4052c3ef38 100644 --- a/apps/www/src/registry/registry-ui.ts +++ b/apps/www/src/registry/registry-ui.ts @@ -250,9 +250,7 @@ export const uiComponents: Registry = [ `import { DndPlugin } from '@udecode/plate-dnd'; import { NodeIdPlugin } from '@udecode/plate-node-id'; import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; - -import { withDraggables } from './withDraggables';`, +import { HTML5Backend } from 'react-dnd-html5-backend';`, `export function MyEditor() { const editor = usePlateEditor({ plugins: [ @@ -261,9 +259,9 @@ import { withDraggables } from './withDraggables';`, DndPlugin.configure({ options: { enableScroller: true } }), ], override: { - components: withDraggables({ + components: { // ...components - }), + }, } }); From 8e4fcbb0b74bf01296500f6f412f7cda6905363c Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 01:28:50 +0100 Subject: [PATCH 02/12] refactor --- .changeset/dnd-major.md | 5 + .../src/lib/static/components/PlateStatic.tsx | 4 +- packages/core/src/react/hooks/index.ts | 1 + packages/core/src/react/hooks/useNodePath.ts | 13 + .../react/plugin/PlateRenderElementProps.ts | 6 +- .../core/src/react/stores/element/index.ts | 1 + .../react/stores/element/useElementStore.ts | 4 +- .../core/src/react/stores/element/usePath.ts | 19 + .../src/react/utils/pipeRenderElement.tsx | 8 +- .../src/react/utils/pluginRenderElement.tsx | 4 +- packages/dnd/src/components/index.ts | 2 - packages/dnd/src/components/useDraggable.ts | 36 +- packages/dnd/src/components/useDropLine.ts | 15 +- .../dnd/src/components/useWithDraggable.ts | 59 --- packages/dnd/src/components/withDraggable.tsx | 32 -- packages/dnd/src/hooks/useDndNode.ts | 9 +- packages/layout/src/lib/index.ts | 1 + packages/layout/src/lib/transforms/index.ts | 3 +- .../src/lib/transforms/insertColumnGroup.ts | 21 +- .../src/lib/transforms/moveMiddleColumn.ts | 20 +- .../layout/src/lib/transforms/resizeColumn.ts | 66 ++++ .../src/lib/transforms/setColumnWidth.ts | 44 --- .../src/lib/transforms/setColumns.spec.tsx | 348 ++++++++++++++++++ .../layout/src/lib/transforms/setColumns.ts | 116 ++++++ .../lib/transforms/toggleColumnGroup.spec.tsx | 183 +++++++++ .../src/lib/transforms/toggleColumnGroup.ts | 62 ++-- packages/layout/src/lib/types.ts | 1 - .../layout/src/lib/utils/columnsToWidths.ts | 4 + packages/layout/src/lib/utils/index.ts | 5 + packages/layout/src/lib/withColumn.ts | 66 ++-- packages/layout/src/react/column-store.ts | 46 --- packages/layout/src/react/index.ts | 1 - packages/plate-utils/src/react/index.ts | 1 + .../plate-utils/src/react/usePlaceholder.ts | 4 + packages/plate-utils/tsconfig.json | 2 +- 35 files changed, 907 insertions(+), 305 deletions(-) create mode 100644 .changeset/dnd-major.md create mode 100644 packages/core/src/react/hooks/useNodePath.ts create mode 100644 packages/core/src/react/stores/element/usePath.ts delete mode 100644 packages/dnd/src/components/useWithDraggable.ts delete mode 100644 packages/dnd/src/components/withDraggable.tsx create mode 100644 packages/layout/src/lib/transforms/resizeColumn.ts delete mode 100644 packages/layout/src/lib/transforms/setColumnWidth.ts create mode 100644 packages/layout/src/lib/transforms/setColumns.spec.tsx create mode 100644 packages/layout/src/lib/transforms/setColumns.ts create mode 100644 packages/layout/src/lib/transforms/toggleColumnGroup.spec.tsx create mode 100644 packages/layout/src/lib/utils/columnsToWidths.ts create mode 100644 packages/layout/src/lib/utils/index.ts delete mode 100644 packages/layout/src/react/column-store.ts diff --git a/.changeset/dnd-major.md b/.changeset/dnd-major.md new file mode 100644 index 0000000000..7d26ff5b60 --- /dev/null +++ b/.changeset/dnd-major.md @@ -0,0 +1,5 @@ +--- +'@udecode/plate-dnd': patch +--- + +w diff --git a/packages/core/src/lib/static/components/PlateStatic.tsx b/packages/core/src/lib/static/components/PlateStatic.tsx index 759e5a3164..64136cb933 100644 --- a/packages/core/src/lib/static/components/PlateStatic.tsx +++ b/packages/core/src/lib/static/components/PlateStatic.tsx @@ -7,7 +7,7 @@ import { type TElement, type TNodeEntry, type TText, - findNode, + findNodePath, getRange, isElement, isInline, @@ -151,7 +151,7 @@ function Children({ return ( {children.map((child, i) => { - const p = findNode(editor, { match: (n) => n === child })?.[1]; + const p = findNodePath(editor, child); let ds: DecoratedRange[] = []; diff --git a/packages/core/src/react/hooks/index.ts b/packages/core/src/react/hooks/index.ts index 341642ca44..55983c6e7f 100644 --- a/packages/core/src/react/hooks/index.ts +++ b/packages/core/src/react/hooks/index.ts @@ -3,4 +3,5 @@ */ export * from './useEditableProps'; +export * from './useNodePath'; export * from './useSlateProps'; diff --git a/packages/core/src/react/hooks/useNodePath.ts b/packages/core/src/react/hooks/useNodePath.ts new file mode 100644 index 0000000000..cc5ddfad8a --- /dev/null +++ b/packages/core/src/react/hooks/useNodePath.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +import type { TNode } from '@udecode/slate'; + +import { findPath } from '@udecode/slate-react'; + +import { useEditorRef } from '../stores'; + +export const useNodePath = (node: TNode) => { + const editor = useEditorRef(); + + return React.useMemo(() => findPath(editor, node), [editor, node]); +}; diff --git a/packages/core/src/react/plugin/PlateRenderElementProps.ts b/packages/core/src/react/plugin/PlateRenderElementProps.ts index 93ad6d15f9..bb0348ad98 100644 --- a/packages/core/src/react/plugin/PlateRenderElementProps.ts +++ b/packages/core/src/react/plugin/PlateRenderElementProps.ts @@ -1,5 +1,6 @@ import type { TElement } from '@udecode/slate'; import type { TRenderElementProps } from '@udecode/slate-react'; +import type { Path } from 'slate'; import type { AnyPluginConfig, PluginConfig } from '../../lib'; import type { PlateRenderNodeProps } from './PlateRenderNodeProps'; @@ -8,4 +9,7 @@ import type { PlateRenderNodeProps } from './PlateRenderNodeProps'; export type PlateRenderElementProps< N extends TElement = TElement, C extends AnyPluginConfig = PluginConfig, -> = PlateRenderNodeProps & TRenderElementProps; +> = PlateRenderNodeProps & + TRenderElementProps & { + path: Path; + }; diff --git a/packages/core/src/react/stores/element/index.ts b/packages/core/src/react/stores/element/index.ts index bdef4fc06d..e3933cf313 100644 --- a/packages/core/src/react/stores/element/index.ts +++ b/packages/core/src/react/stores/element/index.ts @@ -4,3 +4,4 @@ export * from './useElement'; export * from './useElementStore'; +export * from './usePath'; diff --git a/packages/core/src/react/stores/element/useElementStore.ts b/packages/core/src/react/stores/element/useElementStore.ts index 75b5a131a1..90b71fba08 100644 --- a/packages/core/src/react/stores/element/useElementStore.ts +++ b/packages/core/src/react/stores/element/useElementStore.ts @@ -1,4 +1,5 @@ import type { TElement } from '@udecode/slate'; +import type { Path } from 'slate'; import type { Nullable } from '../../../lib'; @@ -6,10 +7,11 @@ import { createAtomStore } from '../../libs/jotai'; export const SCOPE_ELEMENT = 'element'; -export type ElementStoreState = { element: TElement }; +export type ElementStoreState = { element: TElement; path: Path }; const initialState: Nullable = { element: null, + path: null, }; export const { ElementProvider, useElementStore } = createAtomStore( diff --git a/packages/core/src/react/stores/element/usePath.ts b/packages/core/src/react/stores/element/usePath.ts new file mode 100644 index 0000000000..3c69cc30df --- /dev/null +++ b/packages/core/src/react/stores/element/usePath.ts @@ -0,0 +1,19 @@ +import { useEditorRef } from '../plate'; +import { useElementStore } from './useElementStore'; + +/** Get the memoized path of the closest element. */ +export const usePath = (pluginKey?: string) => { + const editor = useEditorRef(); + const value = useElementStore(pluginKey).get.path(); + + if (!value) { + editor.api.debug.warn( + `usePath(${pluginKey}) hook must be used inside the node component's context`, + 'USE_ELEMENT_CONTEXT' + ); + + return undefined; + } + + return value; +}; diff --git a/packages/core/src/react/utils/pipeRenderElement.tsx b/packages/core/src/react/utils/pipeRenderElement.tsx index 8a0da24e8b..d58e399957 100644 --- a/packages/core/src/react/utils/pipeRenderElement.tsx +++ b/packages/core/src/react/utils/pipeRenderElement.tsx @@ -6,6 +6,7 @@ import { DefaultElement } from 'slate-react'; import type { PlateEditor } from '../editor/PlateEditor'; +import { useNodePath } from '../hooks'; import { type RenderElement, pluginRenderElement } from './pluginRenderElement'; /** @see {@link RenderElement} */ @@ -24,15 +25,18 @@ export const pipeRenderElement = ( return function render(props) { let element; + // eslint-disable-next-line react-hooks/rules-of-hooks + const path = useNodePath(props.element)!; + renderElements.some((renderElement) => { - element = renderElement(props as any); + element = renderElement({ ...props, path } as any); return !!element; }); if (element) return element; if (renderElementProp) { - return renderElementProp(props); + return renderElementProp({ ...props, path } as any); } return ( diff --git a/packages/core/src/react/utils/pluginRenderElement.tsx b/packages/core/src/react/utils/pluginRenderElement.tsx index 40e510caef..2fc5c495c6 100644 --- a/packages/core/src/react/utils/pluginRenderElement.tsx +++ b/packages/core/src/react/utils/pluginRenderElement.tsx @@ -81,11 +81,11 @@ export const pluginRenderElement = ( plugin: AnyEditorPlatePlugin ): RenderElement => function render(nodeProps) { - const { element } = nodeProps; + const { element, path } = nodeProps; if (element.type === plugin.node.type) { return ( - + ; + /** The ref of the draggable handle */ + handleRef: ( elementOrNode: Element | React.ReactElement | React.RefObject | null ) => void; - isDragging: boolean; - nodeRef: React.RefObject; }; -export const useDraggableState = ( +export const useDraggable = ( props: UseDndNodeOptions & { element: TElement } ): DraggableState => { const { @@ -22,8 +26,13 @@ export const useDraggableState = ( onDropHandler, } = props; + const editor = useEditorRef(); + const nodeRef = React.useRef(null); + if (!editor.plugins.dnd) return {} as any; + + // eslint-disable-next-line react-hooks/rules-of-hooks const { dragRef, isDragging } = useDndNode({ id: element.id as string, nodeRef, @@ -34,23 +43,8 @@ export const useDraggableState = ( }); return { - dragRef, isDragging, - nodeRef, - }; -}; - -export const useDraggable = (state: DraggableState) => { - return { - previewRef: state.nodeRef, - handleRef: state.dragRef, - }; -}; - -export const useDraggableGutter = () => { - return { - props: { - contentEditable: false, - }, + previewRef: nodeRef, + handleRef: dragRef, }; }; diff --git a/packages/dnd/src/components/useDropLine.ts b/packages/dnd/src/components/useDropLine.ts index cfeb0ef5ff..6efaf1f002 100644 --- a/packages/dnd/src/components/useDropLine.ts +++ b/packages/dnd/src/components/useDropLine.ts @@ -1,5 +1,7 @@ import { useEditorPlugin, useElement } from '@udecode/plate-common/react'; +import type { DropLineDirection } from '../types'; + import { DndPlugin } from '../DndPlugin'; export const useDropLine = ({ @@ -9,7 +11,9 @@ export const useDropLine = ({ /** The id of the element to show the dropline for. */ id?: string; orientation?: 'horizontal' | 'vertical'; -} = {}) => { +} = {}): { + dropLine?: DropLineDirection; +} => { const element = useElement(); const id = idProp || (element.id as string); const dropTarget = useEditorPlugin(DndPlugin).useOption('dropTarget'); @@ -19,9 +23,6 @@ export const useDropLine = ({ if (id && dropTarget?.id !== id) { return { dropLine: '', - props: { - contentEditable: false, - }, }; } if (orientation) { @@ -35,17 +36,11 @@ export const useDropLine = ({ ) { return { dropLine: '', - props: { - contentEditable: false, - }, }; } } return { dropLine, - props: { - contentEditable: false, - }, }; }; diff --git a/packages/dnd/src/components/useWithDraggable.ts b/packages/dnd/src/components/useWithDraggable.ts deleted file mode 100644 index e529cfdf01..0000000000 --- a/packages/dnd/src/components/useWithDraggable.ts +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import type { Path } from 'slate'; - -import { - type PlateEditor, - type PlateRenderElementProps, - findPath, -} from '@udecode/plate-common/react'; -import { useReadOnly } from 'slate-react'; - -export interface WithDraggableOptions { - /** Enables dnd in read-only. */ - allowReadOnly?: boolean; - - draggableProps?: T; - - /** Filter out elements that can't be dragged. */ - filter?: (editor: PlateEditor, path: Path) => boolean; - /** - * Document level where dnd is enabled. 0 = root blocks, 1 = first level of - * children, etc. Set to null to allow all levels. - * - * @default 0 - */ - level?: number | null; -} - -export const useWithDraggable = ({ - allowReadOnly = false, - draggableProps, - editor, - element, - filter, - level = 0, -}: PlateRenderElementProps & WithDraggableOptions) => { - const readOnly = useReadOnly(); - const path = React.useMemo( - () => findPath(editor, element), - [editor, element] - ); - - const filteredOut = React.useMemo( - () => - path && - ((Number.isInteger(level) && level !== path.length - 1) || - filter?.(editor, path)), - [path, level, filter, editor] - ); - - return { - disabled: filteredOut || (!allowReadOnly && readOnly), - draggableProps: { - editor, - element, - ...draggableProps, - }, - }; -}; diff --git a/packages/dnd/src/components/withDraggable.tsx b/packages/dnd/src/components/withDraggable.tsx deleted file mode 100644 index 04ad2eafbc..0000000000 --- a/packages/dnd/src/components/withDraggable.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import type { AnyObject } from '@udecode/plate-common'; -import type { PlateRenderElementProps } from '@udecode/plate-common/react'; - -import { - type WithDraggableOptions, - useWithDraggable, -} from './useWithDraggable'; - -export const withDraggable = ( - Draggable: React.FC, - Component: React.FC, - options?: WithDraggableOptions -) => - // eslint-disable-next-line react/display-name - React.forwardRef((props, ref) => { - const { disabled, draggableProps } = useWithDraggable({ - ...options, - ...props, - }); - - if (disabled) { - return ; - } - - return ( - - - - ); - }); diff --git a/packages/dnd/src/hooks/useDndNode.ts b/packages/dnd/src/hooks/useDndNode.ts index 0cd8b69997..f3f3f97573 100644 --- a/packages/dnd/src/hooks/useDndNode.ts +++ b/packages/dnd/src/hooks/useDndNode.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { NativeTypes, getEmptyImage } from 'react-dnd-html5-backend'; -import type { DropTargetMonitor } from 'react-dnd'; +import type { ConnectDragSource, DropTargetMonitor } from 'react-dnd'; import { type PlateEditor, useEditorRef } from '@udecode/plate-common/react'; @@ -58,8 +58,13 @@ export const useDndNode = ({ preview: previewOptions = {}, type = DRAG_ITEM_BLOCK, onDropHandler, -}: UseDndNodeOptions) => { +}: UseDndNodeOptions): { + dragRef: ConnectDragSource; + isDragging: boolean; + isOver: boolean; +} => { const editor = useEditorRef(); + const [{ isDragging }, dragRef, preview] = useDragNode(editor, { id, type, diff --git a/packages/layout/src/lib/index.ts b/packages/layout/src/lib/index.ts index 34b1e779d8..14119d4244 100644 --- a/packages/layout/src/lib/index.ts +++ b/packages/layout/src/lib/index.ts @@ -6,3 +6,4 @@ export * from './BaseColumnPlugin'; export * from './types'; export * from './withColumn'; export * from './transforms/index'; +export * from './utils/index'; diff --git a/packages/layout/src/lib/transforms/index.ts b/packages/layout/src/lib/transforms/index.ts index 5075b4c4a2..2a5fee6fed 100644 --- a/packages/layout/src/lib/transforms/index.ts +++ b/packages/layout/src/lib/transforms/index.ts @@ -5,5 +5,6 @@ export * from './insertColumn'; export * from './insertColumnGroup'; export * from './moveMiddleColumn'; -export * from './setColumnWidth'; +export * from './resizeColumn'; +export * from './setColumns'; export * from './toggleColumnGroup'; diff --git a/packages/layout/src/lib/transforms/insertColumnGroup.ts b/packages/layout/src/lib/transforms/insertColumnGroup.ts index 8f108769c2..bd81d110ca 100644 --- a/packages/layout/src/lib/transforms/insertColumnGroup.ts +++ b/packages/layout/src/lib/transforms/insertColumnGroup.ts @@ -14,27 +14,26 @@ import { BaseColumnItemPlugin, BaseColumnPlugin } from '../BaseColumnPlugin'; export const insertColumnGroup = ( editor: SlateEditor, { - layout = 2, + columns = 2, select: selectProp, ...options }: InsertNodesOptions & { - layout?: number[] | number; + columns?: number; } = {} ) => { - const columnLayout = Array.isArray(layout) - ? layout - : Array(layout).fill(Math.floor(100 / layout)); + const width = 100 / columns; withoutNormalizing(editor, () => { insertNodes( editor, { - children: columnLayout.map((width) => ({ - children: [editor.api.create.block()], - type: BaseColumnItemPlugin.key, - width: `${width}%`, - })), - layout: columnLayout, + children: Array(columns) + .fill(null) + .map(() => ({ + children: [editor.api.create.block()], + type: BaseColumnItemPlugin.key, + width: `${width}%`, + })), type: BaseColumnPlugin.key, }, options diff --git a/packages/layout/src/lib/transforms/moveMiddleColumn.ts b/packages/layout/src/lib/transforms/moveMiddleColumn.ts index 5ab1b2fff6..117253e33e 100644 --- a/packages/layout/src/lib/transforms/moveMiddleColumn.ts +++ b/packages/layout/src/lib/transforms/moveMiddleColumn.ts @@ -2,17 +2,19 @@ import { type SlateEditor, type TNode, type TNodeEntry, + getNode, + getNodeDescendant, + getNodeString, moveNodes, removeNodes, unwrapNodes, } from '@udecode/plate-common'; -import { Node } from 'slate'; import type { TColumnElement } from '../types'; /** - * Move the middle column to the left of right by options.direction. if the - * middle node is empty return false and remove it. + * Move the middle column to the left if direction is 'left', or to the right if + * 'right'. If the middle node is empty, return false and remove it. */ export const moveMiddleColumn = ( editor: SlateEditor, @@ -26,8 +28,12 @@ export const moveMiddleColumn = ( if (direction === 'left') { const DESCENDANT_PATH = [1]; - const middleChildNode = Node.get(node, DESCENDANT_PATH); - const isEmpty = editor.isEmpty(middleChildNode as any); + const middleChildNode = getNode(node, DESCENDANT_PATH); + + if (!middleChildNode) return false; + + // Check emptiness using Node.string + const isEmpty = getNodeString(middleChildNode) === ''; const middleChildPathRef = editor.pathRef(path.concat(DESCENDANT_PATH)); @@ -37,7 +43,9 @@ export const moveMiddleColumn = ( return false; } - const firstNode = Node.descendant(node, [0]) as TColumnElement; + const firstNode = getNodeDescendant(node, [0]); + + if (!firstNode) return false; const firstLast = path.concat([0, firstNode.children.length]); diff --git a/packages/layout/src/lib/transforms/resizeColumn.ts b/packages/layout/src/lib/transforms/resizeColumn.ts new file mode 100644 index 0000000000..39c3623ecc --- /dev/null +++ b/packages/layout/src/lib/transforms/resizeColumn.ts @@ -0,0 +1,66 @@ +import type { TColumnGroupElement } from '../types'; + +export function resizeColumn( + columnGroup: TColumnGroupElement, + columnId: string, + newWidthPercent: number +): TColumnGroupElement { + // Convert widths to numbers for easier math + const widths = columnGroup.children.map((col) => + col.width ? Number.parseFloat(col.width) : 0 + ); + + const totalBefore = widths.reduce((sum, w) => sum + w, 0); + + // fallback if columns do not sum to 100: normalize them + if (totalBefore === 0) { + // distribute evenly if no widths + const evenWidth = 100 / columnGroup.children.length; + columnGroup.children.forEach((col) => { + col.width = `${evenWidth}%`; + }); + + return columnGroup; + } + + const index = columnGroup.children.findIndex((col) => col.id === columnId); + + if (index === -1) return columnGroup; // Column not found + + // Set the new width for the target column + widths[index] = newWidthPercent; + + // Calculate the difference from total (ideally 100) + let totalAfter = widths.reduce((sum, w) => sum + w, 0); + + // If total is off from 100%, adjust siblings + // For simplicity, assume totalAfter < 100%. Add leftover to the next column. + // You can make this logic more balanced if needed. + const diff = 100 - totalAfter; + + if (diff !== 0) { + // Find a sibling to adjust. For a simple strategy, pick the next column. + const siblingIndex = (index + 1) % widths.length; + widths[siblingIndex] = Math.max(widths[siblingIndex] + diff, 0); + } + + // Normalize again if rounding introduced a small error + totalAfter = widths.reduce((sum, w) => sum + w, 0); + + if (Math.round(totalAfter) !== 100) { + // If you want a perfectly balanced approach: + // Scale all widths so they sum exactly to 100 + const scale = 100 / totalAfter; + + for (let i = 0; i < widths.length; i++) { + widths[i] = Number.parseFloat((widths[i] * scale).toFixed(2)); + } + } + + // Update the column widths + columnGroup.children.forEach((col, i) => { + col.width = `${widths[i]}%`; + }); + + return columnGroup; +} diff --git a/packages/layout/src/lib/transforms/setColumnWidth.ts b/packages/layout/src/lib/transforms/setColumnWidth.ts deleted file mode 100644 index f60abd4b82..0000000000 --- a/packages/layout/src/lib/transforms/setColumnWidth.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { PathRef } from 'slate'; - -import { - type SlateEditor, - getChildren, - getNodeEntry, - isElement, - setNodes, -} from '@udecode/plate-common'; - -import type { TColumnElement, TColumnGroupElement } from '../types'; - -import { BaseColumnItemPlugin } from '../BaseColumnPlugin'; - -export const setColumnWidth = ( - editor: SlateEditor, - groupPathRef: PathRef, - layout: Required['layout'] -) => { - const path = groupPathRef.unref()!; - - const columnGroup = getNodeEntry(editor, path); - - if (!columnGroup) throw new Error(`can not find the column group in ${path}`); - - const children = getChildren(columnGroup); - - const childPaths = Array.from(children, (item) => item[1]); - - childPaths.forEach((item, index) => { - const width = layout[index] + '%'; - - if (!width) return; - - setNodes( - editor, - { width: width }, - { - at: item, - match: (n) => isElement(n) && n.type === BaseColumnItemPlugin.key, - } - ); - }); -}; diff --git a/packages/layout/src/lib/transforms/setColumns.spec.tsx b/packages/layout/src/lib/transforms/setColumns.spec.tsx new file mode 100644 index 0000000000..a851c72dee --- /dev/null +++ b/packages/layout/src/lib/transforms/setColumns.spec.tsx @@ -0,0 +1,348 @@ +import type { Path } from 'slate'; + +import { insertNodes } from '@udecode/plate-common'; +import { createPlateEditor } from '@udecode/plate-common/react'; + +import { BaseColumnItemPlugin, BaseColumnPlugin } from '../BaseColumnPlugin'; +import { setColumns } from './setColumns'; + +describe('setColumns', () => { + let editor: ReturnType; + let columnGroupPath: Path; + + beforeEach(() => { + editor = createPlateEditor({ + plugins: [BaseColumnItemPlugin, BaseColumnPlugin], + // Initial value: a column_group with 2 columns + value: [ + { + children: [ + { + children: [{ children: [{ text: 'Column 1 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + { + children: [{ children: [{ text: 'Column 2 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + ], + type: 'column_group', + }, + ], + }); + columnGroupPath = [0]; + }); + + it('should update widths if same number of columns', () => { + // Currently 2 columns, set new widths for these 2 columns + setColumns(editor, { + at: columnGroupPath, + widths: ['30%', '70%'], + }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + expect(node.children[0].width).toBe('30%'); + expect(node.children[1].width).toBe('70%'); + // Content remains the same + expect(node.children[0].children[0].children[0].text).toBe('Column 1 text'); + expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); + }); + + it('should insert new columns if targetCount > currentCount', () => { + // Currently 2 columns, want 3 columns + setColumns(editor, { + at: columnGroupPath, + widths: ['33%', '33%', '33%'], + }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(3); + + // First two columns updated + expect(node.children[0].width).toBe('33%'); + expect(node.children[1].width).toBe('33%'); + + // New column inserted + expect(node.children[2].width).toBe('33%'); + // Should have a default block inside + expect(node.children[2].children).toHaveLength(1); + expect(node.children[2].children[0].type).toBe('p'); + // Original content untouched in the first two columns + expect(node.children[0].children[0].children[0].text).toBe('Column 1 text'); + expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); + }); + + it('should merge columns and remove extras if targetCount < currentCount', () => { + // Setup initial state with 3 columns + editor.children = [ + { + children: [ + { + children: [{ children: [{ text: 'C1 text' }], type: 'p' }], + type: 'column', + width: '33%', + }, + { + children: [{ children: [{ text: 'C2 text' }], type: 'p' }], + type: 'column', + width: '33%', + }, + { + children: [{ children: [{ text: 'C3 text' }], type: 'p' }], + type: 'column', + width: '33%', + }, + ], + type: 'column_group', + }, + ]; + + // Now reduce to 2 columns + setColumns(editor, { + at: columnGroupPath, + widths: ['50%', '50%'], + }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + // Check widths updated + expect(node.children[0].width).toBe('50%'); + expect(node.children[1].width).toBe('50%'); + + // Content from column 3 should have moved into column 2 + const col1Text = node.children[0].children[0].children[0].text; + const col2TextChildren = node.children[1].children.flatMap( + (n: any) => n.children + ); + const col2Texts = col2TextChildren.map((t: any) => t.text); + + expect(col1Text).toBe('C1 text'); + expect(col2Texts).toContain('C2 text'); + expect(col2Texts).toContain('C3 text'); + + // Column 3 should now be removed + }); + + it('should do nothing if no path is provided', () => { + // Call without at + setColumns(editor, { widths: ['100%'] }); + + const node = editor.children[0] as any; + // Should remain unchanged + expect(node.children).toHaveLength(2); + expect(node.children[0].width).toBe('50%'); + expect(node.children[1].width).toBe('50%'); + }); + + it('should do nothing if node is not found at the given path', () => { + setColumns(editor, { at: [999], widths: ['100%'] }); + + const node = editor.children[0] as any; + // Should remain unchanged + expect(node.children).toHaveLength(2); + expect(node.children[0].width).toBe('50%'); + expect(node.children[1].width).toBe('50%'); + }); + + it('should do nothing if widths array is empty', () => { + setColumns(editor, { at: columnGroupPath, widths: [] }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + // Should remain unchanged + expect(node.children[0].width).toBe('50%'); + expect(node.children[1].width).toBe('50%'); + expect(node.children[0].children[0].children[0].text).toBe('Column 1 text'); + expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); + }); + + it('should handle decimal widths', () => { + setColumns(editor, { at: columnGroupPath, widths: ['33.3%', '66.7%'] }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + expect(node.children[0].width).toBe('33.3%'); + expect(node.children[1].width).toBe('66.7%'); + }); + + it('should handle widths that do not sum to 100%', () => { + setColumns(editor, { at: columnGroupPath, widths: ['40%', '40%'] }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + // Even though this sums to 80%, we do not enforce total width + expect(node.children[0].width).toBe('40%'); + expect(node.children[1].width).toBe('40%'); + }); + + it('should handle multiple toggles without losing content', () => { + // Start: 2 columns + // Toggle to 3 columns + setColumns(editor, { at: columnGroupPath, widths: ['33%', '33%', '34%'] }); + + let node = editor.children[0] as any; + expect(node.children).toHaveLength(3); + expect(node.children[0].children[0].children[0].text).toBe('Column 1 text'); + expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); + expect(node.children[2].width).toBe('34%'); + + // Add some new content in the third column + insertNodes( + editor, + { children: [{ text: 'Column 3 text' }], type: 'p' }, + { + at: [0, 2, 1], + } + ); + + // Toggle back to 2 columns + setColumns(editor, { at: columnGroupPath, widths: ['50%', '50%'] }); + + node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + // Col3 content should have merged into column 2 + expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); + expect(node.children[1].children[1].children[0].text).toBe('Column 3 text'); + + // Toggle again to 3 columns + setColumns(editor, { at: columnGroupPath, widths: ['33%', '33%', '34%'] }); + + node = editor.children[0] as any; + expect(node.children).toHaveLength(3); + // Column 3 added again with empty content + expect(node.children[2].children.length).toBeGreaterThan(0); + // Original content is still preserved in columns 2 + expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); + expect(node.children[1].children[1].children[0].text).toBe('Column 3 text'); + expect(node.children[2].children[0].children[0].text).toBe(''); + }); + + it('should gracefully handle toggling to zero columns (though not practical)', () => { + // Set columns to an empty widths array (no columns) + setColumns(editor, { at: columnGroupPath, widths: [] }); + + // Should have done nothing as per previous test, but let's check stability + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + }); + + it('should append content to the end when merging columns', () => { + // Setup initial state with 3 columns + editor.children = [ + { + children: [ + { + children: [{ children: [{ text: 'Col 1' }], type: 'p' }], + type: 'column', + width: '33%', + }, + { + children: [ + { children: [{ text: '21' }], type: 'p' }, + { children: [{ text: '22' }], type: 'p' }, + { children: [{ text: '23' }], type: 'p' }, + { children: [{ text: '24' }], type: 'p' }, + { children: [{ text: '25' }], type: 'p' }, + ], + type: 'column', + width: '33%', + }, + { + children: [ + { children: [{ text: 'Col 3 first' }], type: 'p' }, + { children: [{ text: 'Col 3 second' }], type: 'p' }, + ], + type: 'column', + width: '33%', + }, + ], + type: 'column_group', + }, + ]; + + // Reduce to 2 columns + setColumns(editor, { + at: columnGroupPath, + widths: ['50%', '50%'], + }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + + // Check column 2's content order + const col2Children = node.children[1].children; + expect(col2Children).toHaveLength(7); + expect(col2Children[0].children[0].text).toBe('21'); + expect(col2Children[1].children[0].text).toBe('22'); + expect(col2Children[2].children[0].text).toBe('23'); + expect(col2Children[3].children[0].text).toBe('24'); + expect(col2Children[4].children[0].text).toBe('25'); + expect(col2Children[5].children[0].text).toBe('Col 3 first'); + expect(col2Children[6].children[0].text).toBe('Col 3 second'); + }); + + it('should correctly merge multiple columns when reducing from 4 to 2', () => { + // Setup initial state with 4 columns + editor.children = [ + { + children: [ + { + children: [{ children: [{ text: 'Col 1' }], type: 'p' }], + type: 'column', + width: '25%', + }, + { + children: [ + { children: [{ text: 'Col 2 first' }], type: 'p' }, + { children: [{ text: 'Col 2 second' }], type: 'p' }, + ], + type: 'column', + width: '25%', + }, + { + children: [ + { children: [{ text: 'Col 3 first' }], type: 'p' }, + { children: [{ text: 'Col 3 second' }], type: 'p' }, + ], + type: 'column', + width: '25%', + }, + { + children: [ + { children: [{ text: 'Col 4 first' }], type: 'p' }, + { children: [{ text: 'Col 4 second' }], type: 'p' }, + ], + type: 'column', + width: '25%', + }, + ], + type: 'column_group', + }, + ]; + + // Reduce to 2 columns + setColumns(editor, { + at: columnGroupPath, + widths: ['50%', '50%'], + }); + + const node = editor.children[0] as any; + expect(node.children).toHaveLength(2); + + // Check column 1's content (should be unchanged) + expect(node.children[0].children[0].children[0].text).toBe('Col 1'); + + // Check column 2's content order (should have content from cols 2, 3, and 4) + const col2Children = node.children[1].children; + expect(col2Children).toHaveLength(6); + expect(col2Children[0].children[0].text).toBe('Col 2 first'); + expect(col2Children[1].children[0].text).toBe('Col 2 second'); + expect(col2Children[2].children[0].text).toBe('Col 3 first'); + expect(col2Children[3].children[0].text).toBe('Col 3 second'); + expect(col2Children[4].children[0].text).toBe('Col 4 first'); + expect(col2Children[5].children[0].text).toBe('Col 4 second'); + }); +}); diff --git a/packages/layout/src/lib/transforms/setColumns.ts b/packages/layout/src/lib/transforms/setColumns.ts new file mode 100644 index 0000000000..5ba2898e4f --- /dev/null +++ b/packages/layout/src/lib/transforms/setColumns.ts @@ -0,0 +1,116 @@ +import type { Path } from 'slate'; + +import { + type SlateEditor, + getNode, + getNodeEntry, + insertNodes, + moveChildren, + removeNodes, + setNodes, +} from '@udecode/plate-common'; + +import type { TColumnElement, TColumnGroupElement } from '../types'; + +import { BaseColumnItemPlugin } from '../BaseColumnPlugin'; +import { columnsToWidths } from '../utils/columnsToWidths'; + +export const setColumns = ( + editor: SlateEditor, + { + at, + columns, + widths, + }: { + /** Column group path */ + at?: Path; + columns?: number; + widths?: string[]; + } +) => { + editor.withoutNormalizing(() => { + if (!at) return; + + widths = widths ?? columnsToWidths({ columns }); + + // If widths is empty, do nothing. + if (widths.length === 0) { + return; + } + + const columnGroup = getNode(editor, at); + + if (!columnGroup) return; + + const { children } = columnGroup; + + const currentCount = children.length; + const targetCount = widths.length; + + if (currentCount === targetCount) { + // Same number of columns: just set widths directly + widths.forEach((width, i) => { + setNodes(editor, { width }, { at: at.concat([i]) }); + }); + + return; + } + if (targetCount > currentCount) { + // Need more columns than we have: insert extra columns at the end + const columnsToAdd = targetCount - currentCount; + const insertPath = at.concat([currentCount]); + + // Insert the extra columns + const newColumns = Array(columnsToAdd) + .fill(null) + .map((_, i) => ({ + children: [editor.api.create.block()], + type: editor.getType(BaseColumnItemPlugin), + width: widths![currentCount + i] || `${100 / targetCount}%`, + })); + + insertNodes(editor, newColumns, { at: insertPath }); + + // Just ensure final widths match exactly + widths.forEach((width, i) => { + setNodes(editor, { width }, { at: at.concat([i]) }); + }); + + return; + } + if (targetCount < currentCount) { + // Need fewer columns than we have: merge extra columns into the last kept column + const keepColumnIndex = targetCount - 1; + const keepColumnPath = at.concat([keepColumnIndex]); + const keepColumnNode = getNode(editor, keepColumnPath); + + if (!keepColumnNode) return; + + const to = keepColumnPath.concat([keepColumnNode.children.length]); + + // Move content from columns beyond keepIndex into keepIndex column + for (let i = currentCount - 1; i > keepColumnIndex; i--) { + const columnPath = at.concat([i]); + const columnEntry = getNodeEntry(editor, columnPath); + + if (!columnEntry) continue; + + moveChildren(editor, { + at: columnEntry[1], + to, + }); + } + + // Remove the now-empty extra columns + // Removing from the end to avoid path shifts + for (let i = currentCount - 1; i > keepColumnIndex; i--) { + removeNodes(editor, { at: at.concat([i]) }); + } + + // Set the final widths + widths.forEach((width, i) => { + setNodes(editor, { width }, { at: at.concat([i]) }); + }); + } + }); +}; diff --git a/packages/layout/src/lib/transforms/toggleColumnGroup.spec.tsx b/packages/layout/src/lib/transforms/toggleColumnGroup.spec.tsx new file mode 100644 index 0000000000..acdd11764e --- /dev/null +++ b/packages/layout/src/lib/transforms/toggleColumnGroup.spec.tsx @@ -0,0 +1,183 @@ +import type { Path } from 'slate'; + +import { getStartPoint, select } from '@udecode/plate-common'; +import { createPlateEditor } from '@udecode/plate-common/react'; + +import { BaseColumnItemPlugin, BaseColumnPlugin } from '../BaseColumnPlugin'; +import { toggleColumnGroup } from './toggleColumnGroup'; + +describe('toggleColumnGroup', () => { + let editor: ReturnType; + let initialValue: any[]; + + beforeEach(() => { + initialValue = [ + { + children: [{ text: 'Some paragraph text' }], + type: 'p', + }, + ]; + + editor = createPlateEditor({ + plugins: [BaseColumnItemPlugin, BaseColumnPlugin], + value: initialValue, + }); + }); + + it('should wrap a paragraph in a column group when toggling from a paragraph', () => { + const at: Path = [0, 0]; // Inside the paragraph text + select(editor, getStartPoint(editor, at)); + + // Toggle to 2 columns + toggleColumnGroup(editor, { columns: 2 }); + + const node: any = editor.children[0]; + expect(node.type).toBe('column_group'); + expect(node.children).toHaveLength(2); + expect(node.children[0].type).toBe('column'); + expect(node.children[1].type).toBe('column'); + expect(node.children[0].children[0].children[0].text).toBe( + 'Some paragraph text' + ); + // The second column should have a newly created block + expect(node.children[1].children[0].type).toBe('p'); + expect(node.children[1].children[0].children[0].text).toBe(''); + }); + + it('should update the number of columns if already a column group', () => { + // Start with a column group of 2 columns + editor.children = [ + { + children: [ + { + children: [{ children: [{ text: 'Col1 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + { + children: [{ children: [{ text: 'Col2 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + ], + type: 'column_group', + }, + ]; + + const columnGroupPath: Path = [0]; + select(editor, getStartPoint(editor, columnGroupPath.concat([0, 0, 0]))); + + // Toggle to 3 columns (from 2 columns) + toggleColumnGroup(editor, { columns: 3 }); + + const node: any = editor.children[0]; + expect(node.type).toBe('column_group'); + expect(node.children).toHaveLength(3); + + // All widths should be adjusted + expect(node.children[0].width).toContain('33.3333'); + expect(node.children[1].width).toContain('33.3333'); + expect(node.children[2].width).toContain('33.3333'); + + // Content from the original 2 columns should still exist + expect(node.children[0].children[0].children[0].text).toBe('Col1 text'); + expect(node.children[1].children[0].children[0].text).toBe('Col2 text'); + + // The new 3rd column should have one empty paragraph + expect(node.children[2].children).toHaveLength(1); + expect(node.children[2].children[0].type).toBe('p'); + expect(node.children[2].children[0].children[0].text).toBe(''); + }); + + it('should preserve content when toggling between different column counts', () => { + // Start with a column group of 2 columns + editor.children = [ + { + children: [ + { + children: [{ children: [{ text: 'Col1 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + { + children: [{ children: [{ text: 'Col2 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + ], + type: 'column_group', + }, + ]; + + const columnGroupPath: Path = [0]; + select(editor, getStartPoint(editor, columnGroupPath)); + + // Toggle to 3 columns + toggleColumnGroup(editor, { columns: 3 }); + let node: any = editor.children[0]; + expect(node.children).toHaveLength(3); + + // Insert content in the third column + editor.apply({ + node: { children: [{ text: 'Col3 extra text' }], type: 'p' }, + path: [0, 2, 1], + type: 'insert_node', + }); + + // Toggle back to 2 columns + toggleColumnGroup(editor, { columns: 2 }); + node = editor.children[0]; + expect(node.children).toHaveLength(2); + + // Col3 content should have merged into column 2 + const col2Texts = node.children[1].children + .flatMap((child: any) => child.children) + .map((child: any) => child.text); + + expect(col2Texts).toContain('Col2 text'); + expect(col2Texts).toContain('Col3 extra text'); + }); + + it('should do nothing if no selection is provided', () => { + // No selection + toggleColumnGroup(editor, { columns: 2 }); + // Should remain a single paragraph + const node = editor.children[0]; + expect(node.type).toBe('p'); + }); + + it('should handle toggling from a selection inside a column as well', () => { + // Start with a column group of 2 columns + editor.children = [ + { + children: [ + { + children: [{ children: [{ text: 'Col1 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + { + children: [{ children: [{ text: 'Col2 text' }], type: 'p' }], + type: 'column', + width: '50%', + }, + ], + type: 'column_group', + }, + ]; + const columnGroupPath: Path = [0]; + // Select inside second column's paragraph + select(editor, getStartPoint(editor, [0, 1, 0, 0])); + + // Toggle to 3 columns + toggleColumnGroup(editor, { columns: 3 }); + const node: any = editor.children[0]; + + expect(node.children).toHaveLength(3); + // Should keep Col1 text and Col2 text + expect(node.children[0].children[0].children[0].text).toBe('Col1 text'); + expect(node.children[1].children[0].children[0].text).toBe('Col2 text'); + // New column + expect(node.children[2].children[0].children[0].text).toBe(''); + }); +}); diff --git a/packages/layout/src/lib/transforms/toggleColumnGroup.ts b/packages/layout/src/lib/transforms/toggleColumnGroup.ts index 671cbfc85f..a5bc388795 100644 --- a/packages/layout/src/lib/transforms/toggleColumnGroup.ts +++ b/packages/layout/src/lib/transforms/toggleColumnGroup.ts @@ -9,40 +9,54 @@ import { } from '@udecode/plate-common'; import { BaseColumnItemPlugin, BaseColumnPlugin } from '../BaseColumnPlugin'; +import { columnsToWidths } from '../utils/columnsToWidths'; +import { setColumns } from './setColumns'; export const toggleColumnGroup = ( editor: SlateEditor, { at, - layout = 2, + columns = 2, + widths, }: Partial, 'nodes'>> & { - layout?: number[] | number; + columns?: number; + widths?: string[]; } = {} ) => { const entry = getBlockAbove(editor, { at }); + const columnGroupEntry = getBlockAbove(editor, { + at, + match: { type: editor.getType(BaseColumnPlugin) }, + }); if (!entry) return; - const [node] = entry; - - const columnLayout = Array.isArray(layout) - ? layout - : Array(layout).fill(Math.floor(100 / layout)); - - const nodes = { - children: columnLayout.map((width, index) => ({ - children: [index === 0 ? node : editor.api.create.block()], - type: BaseColumnItemPlugin.key, - width: `${width}%`, - })), - layout: columnLayout, - type: BaseColumnPlugin.key, - } as TElement; - - replaceNode(editor, { - at: entry[1], - nodes, - }); - - select(editor, getStartPoint(editor, entry[1].concat([0]))); + const [node, path] = entry; + + // Check if the node is already a column_group + if (columnGroupEntry) { + // Node is already a column_group, just update the columns using setColumns + setColumns(editor, { at: columnGroupEntry[1], columns, widths }); + } else { + // Node is not a column_group, wrap it in a column_group + const columnWidths = widths || columnsToWidths({ columns }); + + const nodes = { + children: Array(columns) + .fill(null) + .map((_, index) => ({ + children: [index === 0 ? node : editor.api.create.block()], + type: BaseColumnItemPlugin.key, + width: columnWidths[index], + })), + type: BaseColumnPlugin.key, + } as TElement; + + replaceNode(editor, { + at: path, + nodes, + }); + + select(editor, getStartPoint(editor, path.concat([0]))); + } }; diff --git a/packages/layout/src/lib/types.ts b/packages/layout/src/lib/types.ts index 4b391e159f..b4a757b5c9 100644 --- a/packages/layout/src/lib/types.ts +++ b/packages/layout/src/lib/types.ts @@ -10,5 +10,4 @@ export interface TColumnGroupElement extends TElement { children: TColumnElement[]; type: 'column_group'; id?: string; - layout?: number[]; } diff --git a/packages/layout/src/lib/utils/columnsToWidths.ts b/packages/layout/src/lib/utils/columnsToWidths.ts new file mode 100644 index 0000000000..db9e1e415b --- /dev/null +++ b/packages/layout/src/lib/utils/columnsToWidths.ts @@ -0,0 +1,4 @@ +export const columnsToWidths = ({ columns = 2 }: { columns?: number } = {}) => + Array(columns) + .fill(null) + .map((_, i) => `${100 / columns}%`); diff --git a/packages/layout/src/lib/utils/index.ts b/packages/layout/src/lib/utils/index.ts new file mode 100644 index 0000000000..0a70ca33f6 --- /dev/null +++ b/packages/layout/src/lib/utils/index.ts @@ -0,0 +1,5 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './columnsToWidths'; diff --git a/packages/layout/src/lib/withColumn.ts b/packages/layout/src/lib/withColumn.ts index 1c864e56d8..0a0450fdaf 100644 --- a/packages/layout/src/lib/withColumn.ts +++ b/packages/layout/src/lib/withColumn.ts @@ -1,8 +1,6 @@ import { type ExtendEditor, - createPathRef, getAboveNode, - getLastChildPath, isCollapsed, isElement, isStartPoint, @@ -13,17 +11,18 @@ import { import type { TColumnElement, TColumnGroupElement } from './types'; import { BaseColumnItemPlugin, BaseColumnPlugin } from './BaseColumnPlugin'; -import { insertColumn, moveMiddleColumn, setColumnWidth } from './transforms'; export const withColumn: ExtendEditor = ({ editor }) => { - const { deleteBackward, isEmpty, normalizeNode } = editor; + const { deleteBackward, normalizeNode } = editor; editor.normalizeNode = (entry) => { const [n, path] = entry; + // If it's a column group, ensure it has valid children if (isElement(n) && n.type === BaseColumnPlugin.key) { const node = n as TColumnGroupElement; + // If no columns found, unwrap the column group if ( !node.children.some( (child) => @@ -35,6 +34,7 @@ export const withColumn: ExtendEditor = ({ editor }) => { return; } + // If only one column remains, unwrap the group (optional logic) if (node.children.length < 2) { editor.withoutNormalizing(() => { unwrapNodes(editor, { at: path }); @@ -43,39 +43,41 @@ export const withColumn: ExtendEditor = ({ editor }) => { return; } + } - const prevChildrenCnt = node.children.length; - const currentLayout = node.layout; + // const prevChildrenCnt = node.children.length; + // const currentLayout = node.layout; - if (currentLayout) { - const currentChildrenCnt = currentLayout.length; + // if (currentLayout) { + // const currentChildrenCnt = currentLayout.length; - const groupPathRef = createPathRef(editor, path); + // const groupPathRef = createPathRef(editor, path); - if (prevChildrenCnt === 2 && currentChildrenCnt === 3) { - const lastChildPath = getLastChildPath(entry); + // if (prevChildrenCnt === 2 && currentChildrenCnt === 3) { + // const lastChildPath = getLastChildPath(entry); - insertColumn(editor, { - at: lastChildPath, - }); + // insertColumn(editor, { + // at: lastChildPath, + // }); - setColumnWidth(editor, groupPathRef, currentLayout); + // setColumnWidth(editor, groupPathRef, currentLayout); - return; - } - if (prevChildrenCnt === 3 && currentChildrenCnt === 2) { - moveMiddleColumn(editor, entry, { direction: 'left' }); - setColumnWidth(editor, groupPathRef, currentLayout); + // return; + // } + // if (prevChildrenCnt === 3 && currentChildrenCnt === 2) { + // moveMiddleColumn(editor, entry, { direction: 'left' }); + // setColumnWidth(editor, groupPathRef, currentLayout); - return; - } - if (prevChildrenCnt === currentChildrenCnt) { - setColumnWidth(editor, groupPathRef, currentLayout); + // return; + // } + // if (prevChildrenCnt === currentChildrenCnt) { + // setColumnWidth(editor, groupPathRef, currentLayout); - return; - } - } - } + // return; + // } + // } + + // If it's a column, ensure it has at least one block (optional) if (isElement(n) && n.type === BaseColumnItemPlugin.key) { const node = n as TColumnElement; @@ -109,13 +111,5 @@ export const withColumn: ExtendEditor = ({ editor }) => { deleteBackward(unit); }; - editor.isEmpty = (element: any) => { - if (element?.type && element.type === BaseColumnItemPlugin.key) { - return element.children.length === 1 && isEmpty(element.children[0]); - } - - return isEmpty(element); - }; - return editor; }; diff --git a/packages/layout/src/react/column-store.ts b/packages/layout/src/react/column-store.ts deleted file mode 100644 index 5cdbb13831..0000000000 --- a/packages/layout/src/react/column-store.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { setNodes } from '@udecode/plate-common'; -import { - findPath, - useEditorRef, - useElement, -} from '@udecode/plate-common/react'; - -import type { TColumnGroupElement } from '../lib/types'; - -import { ColumnPlugin } from './ColumnPlugin'; - -export const useColumnState = () => { - const editor = useEditorRef(); - - const columnGroupElement = useElement(ColumnPlugin.key); - - const columnPath = findPath(editor, columnGroupElement); - - const setDoubleColumn = () => { - setNodes(editor, { layout: [50, 50] }, { at: columnPath }); - }; - - const setThreeColumn = () => { - setNodes(editor, { layout: [33, 33, 33] }, { at: columnPath }); - }; - - const setRightSideDoubleColumn = () => { - setNodes(editor, { layout: [70, 30] }, { at: columnPath }); - }; - - const setLeftSideDoubleColumn = () => { - setNodes(editor, { layout: [30, 70] }, { at: columnPath }); - }; - - const setDoubleSideDoubleColumn = () => { - setNodes(editor, { layout: [25, 50, 25] }, { at: columnPath }); - }; - - return { - setDoubleColumn, - setDoubleSideDoubleColumn, - setLeftSideDoubleColumn, - setRightSideDoubleColumn, - setThreeColumn, - }; -}; diff --git a/packages/layout/src/react/index.ts b/packages/layout/src/react/index.ts index f9190c9f6e..757a6f621e 100644 --- a/packages/layout/src/react/index.ts +++ b/packages/layout/src/react/index.ts @@ -3,6 +3,5 @@ */ export * from './ColumnPlugin'; -export * from './column-store'; export * from './onKeyDownColumn'; export * from './hooks/index'; diff --git a/packages/plate-utils/src/react/index.ts b/packages/plate-utils/src/react/index.ts index 030315ae3b..6aca70993b 100644 --- a/packages/plate-utils/src/react/index.ts +++ b/packages/plate-utils/src/react/index.ts @@ -14,6 +14,7 @@ export * from './useFormInputProps'; export * from './useLastBlock'; export * from './useLastBlockDOMNode'; export * from './useMarkToolbarButton'; +export * from '../../../core/src/react/hooks/useNodePath'; export * from './usePlaceholder'; export * from './useRemoveNodeButton'; export * from './useSelection'; diff --git a/packages/plate-utils/src/react/usePlaceholder.ts b/packages/plate-utils/src/react/usePlaceholder.ts index 5a7225cd7c..1ef8a468b4 100644 --- a/packages/plate-utils/src/react/usePlaceholder.ts +++ b/packages/plate-utils/src/react/usePlaceholder.ts @@ -10,6 +10,8 @@ import { useComposing, useFocused, useSelected } from 'slate-react'; import type { PlateElementProps } from './PlateElement'; +import { useElementPath } from '../../../core/src/react/hooks/useNodePath'; + export interface PlaceholderProps extends PlateElementProps { placeholder: string; hideOnBlur?: boolean; @@ -28,6 +30,8 @@ export const usePlaceholderState = ({ const isEmptyBlock = isElementEmpty(editor, element) && !composing; + const path = useElementPath(); + const enabled = isEmptyBlock && (!query || queryNode([element, findPath(editor, element)!], query)) && diff --git a/packages/plate-utils/tsconfig.json b/packages/plate-utils/tsconfig.json index ad83d092a5..db0d69cff6 100644 --- a/packages/plate-utils/tsconfig.json +++ b/packages/plate-utils/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../config/tsconfig.base.json", - "include": ["src"], + "include": ["src", "../core/src/react/hooks/useNodePath.ts"], "exclude": [] } From 458f4e2bf09088a52758731be80ad0d50dc54fd2 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 01:28:53 +0100 Subject: [PATCH 03/12] docs --- .changeset/dnd-major.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/dnd-major.md b/.changeset/dnd-major.md index 7d26ff5b60..55935a58f8 100644 --- a/.changeset/dnd-major.md +++ b/.changeset/dnd-major.md @@ -2,4 +2,6 @@ '@udecode/plate-dnd': patch --- -w +Additional breaking changes to v41: + +- From 3566dcdfbac88df49705af27f8527830292722d7 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 02:04:39 +0100 Subject: [PATCH 04/12] refactor --- apps/www/src/registry/registry-components.ts | 1 + packages/indent-list/src/lib/queries/index.ts | 2 ++ packages/indent-list/src/react/hooks/index.ts | 2 -- packages/plate-utils/src/react/index.ts | 1 - packages/plate-utils/src/react/usePlaceholder.ts | 8 ++------ packages/plate-utils/tsconfig.json | 2 +- packages/resizable/src/components/Resizable.tsx | 13 ++++--------- .../selection/src/react/hooks/useBlockSelectable.ts | 9 +++------ .../components/TableElement/useTableColSizes.ts | 7 ++++--- 9 files changed, 17 insertions(+), 28 deletions(-) diff --git a/apps/www/src/registry/registry-components.ts b/apps/www/src/registry/registry-components.ts index 9770123e7c..78e1c81f94 100644 --- a/apps/www/src/registry/registry-components.ts +++ b/apps/www/src/registry/registry-components.ts @@ -252,6 +252,7 @@ const plugins: Registry = [ }, ], name: 'dnd-plugins', + registryDependencies: ['draggable'], type: 'registry:component', }, { diff --git a/packages/indent-list/src/lib/queries/index.ts b/packages/indent-list/src/lib/queries/index.ts index 21d48df66c..690ede7a62 100644 --- a/packages/indent-list/src/lib/queries/index.ts +++ b/packages/indent-list/src/lib/queries/index.ts @@ -8,3 +8,5 @@ export * from './getNextIndentList'; export * from './getPreviousIndentList'; export * from './getSiblingIndentList'; export * from './getSiblingListStyleType'; +export * from './someIndentList'; +export * from './someIndentTodo'; diff --git a/packages/indent-list/src/react/hooks/index.ts b/packages/indent-list/src/react/hooks/index.ts index 29070fc0ee..3dcf9a90f9 100644 --- a/packages/indent-list/src/react/hooks/index.ts +++ b/packages/indent-list/src/react/hooks/index.ts @@ -2,8 +2,6 @@ * @file Automatically generated by barrelsby. */ -export * from '../../lib/queries/someIndentList'; -export * from '../../lib/queries/someIndentTodo'; export * from './useIndentListToolbarButton'; export * from './useIndentTodoListElement'; export * from './useIndentTodoToolbarButton'; diff --git a/packages/plate-utils/src/react/index.ts b/packages/plate-utils/src/react/index.ts index 6aca70993b..030315ae3b 100644 --- a/packages/plate-utils/src/react/index.ts +++ b/packages/plate-utils/src/react/index.ts @@ -14,7 +14,6 @@ export * from './useFormInputProps'; export * from './useLastBlock'; export * from './useLastBlockDOMNode'; export * from './useMarkToolbarButton'; -export * from '../../../core/src/react/hooks/useNodePath'; export * from './usePlaceholder'; export * from './useRemoveNodeButton'; export * from './useSelection'; diff --git a/packages/plate-utils/src/react/usePlaceholder.ts b/packages/plate-utils/src/react/usePlaceholder.ts index 1ef8a468b4..497e26764a 100644 --- a/packages/plate-utils/src/react/usePlaceholder.ts +++ b/packages/plate-utils/src/react/usePlaceholder.ts @@ -5,13 +5,10 @@ import { isElementEmpty, queryNode, } from '@udecode/slate'; -import { findPath } from '@udecode/slate-react'; import { useComposing, useFocused, useSelected } from 'slate-react'; import type { PlateElementProps } from './PlateElement'; -import { useElementPath } from '../../../core/src/react/hooks/useNodePath'; - export interface PlaceholderProps extends PlateElementProps { placeholder: string; hideOnBlur?: boolean; @@ -21,6 +18,7 @@ export interface PlaceholderProps extends PlateElementProps { export const usePlaceholderState = ({ element, hideOnBlur = true, + path, query, }: PlaceholderProps) => { const focused = useFocused(); @@ -30,11 +28,9 @@ export const usePlaceholderState = ({ const isEmptyBlock = isElementEmpty(editor, element) && !composing; - const path = useElementPath(); - const enabled = isEmptyBlock && - (!query || queryNode([element, findPath(editor, element)!], query)) && + (!query || queryNode([element, path!], query)) && (!hideOnBlur || (isCollapsed(editor.selection) && hideOnBlur && focused && selected)); diff --git a/packages/plate-utils/tsconfig.json b/packages/plate-utils/tsconfig.json index db0d69cff6..ad83d092a5 100644 --- a/packages/plate-utils/tsconfig.json +++ b/packages/plate-utils/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../config/tsconfig.base.json", - "include": ["src", "../core/src/react/hooks/useNodePath.ts"], + "include": ["src"], "exclude": [] } diff --git a/packages/resizable/src/components/Resizable.tsx b/packages/resizable/src/components/Resizable.tsx index 0bd5a4338e..71de0d871b 100644 --- a/packages/resizable/src/components/Resizable.tsx +++ b/packages/resizable/src/components/Resizable.tsx @@ -1,11 +1,7 @@ import React from 'react'; import { select, setNodes } from '@udecode/plate-common'; -import { - findPath, - useEditorRef, - useElement, -} from '@udecode/plate-common/react'; +import { useEditorRef, useElement, usePath } from '@udecode/plate-common/react'; import type { ResizeEvent, ResizeLength } from '../types'; import type { TResizableElement } from './TResizableElement'; @@ -29,8 +25,9 @@ export const useResizableState = ({ maxWidth = '100%', minWidth = 92, }: ResizableOptions = {}) => { - const element = useElement(); const editor = useEditorRef(); + const element = useElement(); + const path = usePath(); const nodeWidth = element?.width ?? '100%'; @@ -38,8 +35,6 @@ export const useResizableState = ({ const setNodeWidth = React.useCallback( (w: number) => { - const path = findPath(editor, element!); - if (!path) return; if (w === nodeWidth) { // Focus the node if not resized @@ -48,7 +43,7 @@ export const useResizableState = ({ setNodes(editor, { width: w }, { at: path }); } }, - [editor, element, nodeWidth] + [editor, nodeWidth, path] ); React.useEffect(() => { diff --git a/packages/selection/src/react/hooks/useBlockSelectable.ts b/packages/selection/src/react/hooks/useBlockSelectable.ts index 55034b4e73..d0b3265b78 100644 --- a/packages/selection/src/react/hooks/useBlockSelectable.ts +++ b/packages/selection/src/react/hooks/useBlockSelectable.ts @@ -1,10 +1,10 @@ -import React from 'react'; +import type React from 'react'; import { getAboveNode, isVoid } from '@udecode/plate-common'; import { - findPath, useEditorPlugin, useElement, + usePath, } from '@udecode/plate-common/react'; import { Path } from 'slate'; @@ -12,12 +12,9 @@ import { BlockSelectionPlugin } from '../BlockSelectionPlugin'; export const useBlockSelectable = () => { const element = useElement(); + const path = usePath(); const { api, editor, getOption, getOptions } = useEditorPlugin(BlockSelectionPlugin); - const path = React.useMemo( - () => findPath(editor, element), - [editor, element] - ); const id = element?.id as string | undefined; diff --git a/packages/table/src/react/components/TableElement/useTableColSizes.ts b/packages/table/src/react/components/TableElement/useTableColSizes.ts index ce8f7d7df6..e89074af71 100644 --- a/packages/table/src/react/components/TableElement/useTableColSizes.ts +++ b/packages/table/src/react/components/TableElement/useTableColSizes.ts @@ -1,7 +1,7 @@ import React from 'react'; import { unsetNodes } from '@udecode/plate-common'; -import { findPath, useEditorRef } from '@udecode/plate-common/react'; +import { useEditorRef, useNodePath } from '@udecode/plate-common/react'; import { type TTableElement, @@ -21,6 +21,7 @@ export const useTableColSizes = ( ): number[] => { const editor = useEditorRef(); const colSizeOverrides = useTableStore().get.colSizeOverrides(); + const path = useNodePath(tableNode); const { enableUnsetSingleColSize } = editor.getOptions(TablePlugin); @@ -38,10 +39,10 @@ export const useTableColSizes = ( tableNode.colSizes?.length ) { unsetNodes(editor, 'colSizes', { - at: findPath(editor, tableNode), + at: path, }); } - }, [colCount, enableUnsetSingleColSize, editor, tableNode]); + }, [colCount, enableUnsetSingleColSize, editor, tableNode, path]); return overriddenColSizes; }; From e14fb8854fde9ce2e6ddfb115144c451a4f7c8f0 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 02:29:03 +0100 Subject: [PATCH 05/12] docs --- .changeset/dnd-major copy 2.md | 8 ++ .changeset/dnd-major copy 3.md | 8 ++ .changeset/dnd-major copy.md | 7 ++ .changeset/dnd-major.md | 5 +- apps/www/content/docs/column.mdx | 87 ++++++++++--------- .../www/content/docs/components/changelog.mdx | 20 +++++ apps/www/content/docs/dnd.mdx | 45 ++-------- 7 files changed, 102 insertions(+), 78 deletions(-) create mode 100644 .changeset/dnd-major copy 2.md create mode 100644 .changeset/dnd-major copy 3.md create mode 100644 .changeset/dnd-major copy.md diff --git a/.changeset/dnd-major copy 2.md b/.changeset/dnd-major copy 2.md new file mode 100644 index 0000000000..ecfecaef24 --- /dev/null +++ b/.changeset/dnd-major copy 2.md @@ -0,0 +1,8 @@ +--- +'@udecode/plate-layout': patch +--- + +- Add `setColumns`: set any number of columns of any size to a column group. Decreasing the number of columns will move the removed columns' content to the last remaining column. +- `toggleColumnGroup`: now uses `setColumns` if we're already in a column group. +- Remove `layout` prop from `column_group` nodes. We're now only relying on `column` `width` prop. You can unset `layout` prop or just leave it as it is since it's not read anymore. +- `ColumnPlugin`: Added width normalization ensuring column widths always sum to 100% by automatically adjusting widths when columns are added or removed. If the sum of widths is not 100%, the difference is distributed evenly across all columns. diff --git a/.changeset/dnd-major copy 3.md b/.changeset/dnd-major copy 3.md new file mode 100644 index 0000000000..2e66e8c653 --- /dev/null +++ b/.changeset/dnd-major copy 3.md @@ -0,0 +1,8 @@ +--- +'@udecode/plate-layout': patch +--- + +Additional breaking changes to v41: + +- `insertColumnGroup`: rename `layout` to `columns` +- Remove `setColumnWidth`, `useColumnState`. Use `setColumns` instead diff --git a/.changeset/dnd-major copy.md b/.changeset/dnd-major copy.md new file mode 100644 index 0000000000..8f1ed867ba --- /dev/null +++ b/.changeset/dnd-major copy.md @@ -0,0 +1,7 @@ +--- +'@udecode/plate-core': patch +--- + +- Add `useNodePath(node: TNode)`: memoized `findPath` (`useMemo`) +- Add `usePath(pluginKey?: string)`: memoized `findPath` (context) +- `PlateElementProps` now includes `path` prop, also accessible using `usePath` diff --git a/.changeset/dnd-major.md b/.changeset/dnd-major.md index 55935a58f8..18c331315c 100644 --- a/.changeset/dnd-major.md +++ b/.changeset/dnd-major.md @@ -4,4 +4,7 @@ Additional breaking changes to v41: -- +- Remove `useDraggableState`. Use `const { isDragging, previewRef, handleRef } = useDraggable` +- Remove `useDraggableGutter`. Set `contentEditable={false}` to your gutter element +- Remove `props` from `useDropLine`. Set `contentEditable={false}` to your drop line element +- Remove `withDraggable`, `useWithDraggable`. Use [`DraggableAboveNodes`](https://github.com/udecode/plate/pull/3878/files#diff-493c12ebed9c3ef9fd8c3a723909b18ad439a448c0132d2d93e5341ee0888ad2) instead diff --git a/apps/www/content/docs/column.mdx b/apps/www/content/docs/column.mdx index 310c0d55c2..86bf82a789 100644 --- a/apps/www/content/docs/column.mdx +++ b/apps/www/content/docs/column.mdx @@ -53,9 +53,6 @@ Add Column Item Plugin to your document. Extends `TElement`. - - Set the width of its children `Column` - ### TColumnElement @@ -76,9 +73,9 @@ Insert a columnGroup with two empty columns. The editor instance. - - - `layout`: Array of column widths or number of equal-width columns (default: 2) - - Other InsertNodesOptions to control insert behavior + + - `columns`: Array of column widths or number of equal-width columns (default: 2) + - Other `InsertNodesOptions` to control insert behavior @@ -111,59 +108,69 @@ Move the middle column to the left or right by options.direction. If the middle -### setColumnWidth +### toggleColumnGroup -Set the width of `ColumnGroup`'s children. In general, you don't need to use this function, since we will call this function automatically when the property of `layout` changes. -If you want to set the `layout` use setNodes. +Converts a block into a column group layout or updates an existing column group's layout. The editor instance. - - The path ref of `ColumnGroup` - - - The element property of `layout` + + + + The location to toggle the column group at. + + + Number of equal-width columns to create (default: 2) + + + Array of column widths (e.g., ['50%', '50%']). Takes precedence over `columns`. + + -### toggleColumnGroup +Behavior: +- If the target block is not a column group, wraps it in a new column group with the specified number of columns +- If the target block is already a column group, updates its column layout using `setColumns` +- The original content becomes the content of the first column +- Additional columns are created with empty paragraphs -Convert a block into a column group layout. The selected block becomes the content of the first column. +### setColumns + +Updates the column layout of an existing column group by modifying the number and widths of columns. The editor instance. - - - `layout`: Array of column widths or number of equal-width columns (default: 2) + + + + The path to the column group element. + + + Number of equal-width columns to create. + + + Array of column widths (e.g., ['33%', '67%']). Takes precedence over `columns`. + + -## API Components - -### useColumnState +Behavior: +- When increasing columns: + - Keeps existing column content + - Adds new empty columns with specified widths +- When decreasing columns: + - Merges content from removed columns into the last remaining column + - Updates widths of remaining columns +- When keeping same number of columns: + - Only updates column widths - - - Call this function to make the `ColumnGroup`'s children, column bisects the - parent element space. This sets the `layout` property of - `ColumnGroup` to `[50,50]` - - - Set the `layout` property of `ColumnGroup` to `[33, 33, 33]` - - - Set the `layout` property of `ColumnGroup` to `[70,30]` - - - Set the `layout` property of `ColumnGroup` to `[30,70]` - - - Set the `layout` property of `ColumnGroup` to `[25, 50, 25]` - - +## API Components ### useDebouncePopoverOpen diff --git a/apps/www/content/docs/components/changelog.mdx b/apps/www/content/docs/components/changelog.mdx index fa2590eb44..291de671e3 100644 --- a/apps/www/content/docs/components/changelog.mdx +++ b/apps/www/content/docs/components/changelog.mdx @@ -11,6 +11,26 @@ Use the [CLI](https://platejs.org/docs/components/cli) to install the latest ver ## December 2024 #17 +### December 20 #17.3 + +- `insertColumnGroup`, `toggleColumnGroup`: use `columns` option instead of `layout` +- Remove `with-draggables`. Add [`DraggableAboveNodes`](https://github.com/udecode/plate/pull/3878/files#diff-493c12ebed9c3ef9fd8c3a723909b18ad439a448c0132d2d93e5341ee0888ad2) to `draggable`. Add to `DndPlugin` config: +```tsx +DndPlugin.configure({ render: { aboveNodes: DraggableAboveNodes } }), +``` +- `column-element`, `image-element`, `media-video-element`: Remove `useDraggableState`. Use `const { isDragging, previewRef, handleRef } = useDraggable` +- `column-group-element`: Remove `useColumnState`. Use instead: +```tsx +const columnGroupElement = useElement(ColumnPlugin.key); + +const onColumnChange = (widths: string[]) => { + setColumns(editor, { + at: findNodePath(editor, columnGroupElement), + widths, + }); +}; +``` + ### December 19 #17.2 Plate 41 diff --git a/apps/www/content/docs/dnd.mdx b/apps/www/content/docs/dnd.mdx index 27897f6422..6d0b7a2d62 100644 --- a/apps/www/content/docs/dnd.mdx +++ b/apps/www/content/docs/dnd.mdx @@ -269,23 +269,23 @@ A custom hook that enables dragging of a node from the editor using the `useDrag -### useDraggableState +### useDraggable A custom hook that manages the draggable state for a node. - + - + The element to make draggable. - + The orientation of drag and drop. Defaults to `'vertical'`. - + The type of drag item. Defaults to `'block'`. - + Handler called when the element is dropped. @@ -293,36 +293,17 @@ A custom hook that manages the draggable state for a node. - + The drag source connector function. Whether the element is currently being dragged. - + Reference to the draggable element. -### useDraggable - -A custom hook that provides the necessary properties and event handlers for making an element draggable. - - - - The state returned from `useDraggableState`. - - - - - - A reference to the HTML `div` element that serves as the preview during drag. - - - A reference to the drag source connector provided by `react-dnd`. - - - ### useDropNode A custom hook that enables dropping a node on the editor. It uses the `useDrop` hook from `react-dnd` to handle the drop behavior. @@ -359,16 +340,6 @@ A custom hook that enables dropping a node on the editor. It uses the `useDrop` -### useDraggableGutter - -Returns props for the draggable gutter. - - - - Props to be spread on the gutter element. - - - ### useDropLine Returns the current drop line state for an element. From 9f9cea27aca039904d85b592e1aad64cb5585744 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 02:29:09 +0100 Subject: [PATCH 06/12] clean --- packages/cloud/.npmignore | 3 - packages/cloud/CHANGELOG.md | 217 ------------------ packages/cloud/README.md | 24 -- packages/cloud/package.json | 67 ------ .../src/attachment/CloudAttachmentPlugin.ts | 67 ------ packages/cloud/src/attachment/index.ts | 7 - packages/cloud/src/attachment/types.ts | 10 - .../useCloudAttachmentElementState.ts | 52 ----- packages/cloud/src/cloud/CloudPlugin.ts | 75 ------ packages/cloud/src/cloud/finishUploads.ts | 31 --- .../cloud/src/cloud/generateSrcAndSrcSet.ts | 61 ----- .../cloud/src/cloud/getInProgressUploads.ts | 94 -------- packages/cloud/src/cloud/getSaveValue.ts | 108 --------- packages/cloud/src/cloud/handlers.ts | 49 ---- packages/cloud/src/cloud/index.ts | 12 - packages/cloud/src/cloud/types.ts | 64 ------ packages/cloud/src/cloud/uploadFiles.ts | 108 --------- packages/cloud/src/image/CloudImagePlugin.ts | 94 -------- packages/cloud/src/image/index.ts | 7 - packages/cloud/src/image/types.ts | 10 - .../src/image/useCloudImageElementState.ts | 74 ------ packages/cloud/src/index.ts | 8 - .../cloud/src/upload/createUploadStore.ts | 32 --- packages/cloud/src/upload/index.ts | 7 - packages/cloud/src/upload/types.ts | 53 ----- packages/cloud/src/upload/useUpload.ts | 38 --- packages/cloud/tsconfig.build.json | 8 - packages/cloud/tsconfig.json | 5 - 28 files changed, 1385 deletions(-) delete mode 100644 packages/cloud/.npmignore delete mode 100644 packages/cloud/CHANGELOG.md delete mode 100644 packages/cloud/README.md delete mode 100644 packages/cloud/package.json delete mode 100644 packages/cloud/src/attachment/CloudAttachmentPlugin.ts delete mode 100644 packages/cloud/src/attachment/index.ts delete mode 100644 packages/cloud/src/attachment/types.ts delete mode 100644 packages/cloud/src/attachment/useCloudAttachmentElementState.ts delete mode 100644 packages/cloud/src/cloud/CloudPlugin.ts delete mode 100644 packages/cloud/src/cloud/finishUploads.ts delete mode 100644 packages/cloud/src/cloud/generateSrcAndSrcSet.ts delete mode 100644 packages/cloud/src/cloud/getInProgressUploads.ts delete mode 100644 packages/cloud/src/cloud/getSaveValue.ts delete mode 100644 packages/cloud/src/cloud/handlers.ts delete mode 100644 packages/cloud/src/cloud/index.ts delete mode 100644 packages/cloud/src/cloud/types.ts delete mode 100644 packages/cloud/src/cloud/uploadFiles.ts delete mode 100644 packages/cloud/src/image/CloudImagePlugin.ts delete mode 100644 packages/cloud/src/image/index.ts delete mode 100644 packages/cloud/src/image/types.ts delete mode 100644 packages/cloud/src/image/useCloudImageElementState.ts delete mode 100644 packages/cloud/src/index.ts delete mode 100644 packages/cloud/src/upload/createUploadStore.ts delete mode 100644 packages/cloud/src/upload/index.ts delete mode 100644 packages/cloud/src/upload/types.ts delete mode 100644 packages/cloud/src/upload/useUpload.ts delete mode 100644 packages/cloud/tsconfig.build.json delete mode 100644 packages/cloud/tsconfig.json diff --git a/packages/cloud/.npmignore b/packages/cloud/.npmignore deleted file mode 100644 index 7d3b305b17..0000000000 --- a/packages/cloud/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -__tests__ -__test-utils__ -__mocks__ diff --git a/packages/cloud/CHANGELOG.md b/packages/cloud/CHANGELOG.md deleted file mode 100644 index c2d5c8f244..0000000000 --- a/packages/cloud/CHANGELOG.md +++ /dev/null @@ -1,217 +0,0 @@ -# @udecode/plate-cloud - -## 41.0.0 - -## 40.0.0 - -## 39.0.0 - -## 38.0.1 - -### Patch Changes - -- [#3526](https://github.com/udecode/plate/pull/3526) by [@zbeyens](https://github.com/zbeyens) – Prefix base plugin with `Base` - -## 38.0.0 - -## 37.0.0 - -### Major Changes - -- [#3420](https://github.com/udecode/plate/pull/3420) by [@zbeyens](https://github.com/zbeyens) – - - `createCloudPlugin` -> `CloudPlugin` - -## 36.0.0 - -## 34.0.0 - -## 33.0.2 - -### Patch Changes - -- [#3187](https://github.com/udecode/plate/pull/3187) by [@zbeyens](https://github.com/zbeyens) – Fix types - -## 33.0.0 - -## 32.0.0 - -## 31.0.0 - -## 30.7.1 - -### Patch Changes - -- [#3003](https://github.com/udecode/plate/pull/3003) by [@Lenghak](https://github.com/Lenghak) – fix: #3002 - -## 30.5.3 - -### Patch Changes - -- [`4cbed7159`](https://github.com/udecode/plate/commit/4cbed7159d51f7427051686e45bcf2a8899aeede) by [@zbeyens](https://github.com/zbeyens) – Move `@udecode/plate-common` to peerDeps to fix a bug when multiple instances were installed - -## 30.5.2 - -### Patch Changes - -- [#2961](https://github.com/udecode/plate/pull/2961) by [@zbeyens](https://github.com/zbeyens) – Move `@udecode/plate-common` to peerDeps to fix a bug when multiple instances were installed - -## 30.4.5 - -## 30.1.2 - -## 30.0.0 - -## 29.1.0 - -## 29.0.1 - -## 29.0.0 - -## 28.0.0 - -## 27.0.3 - -## 27.0.0 - -### Patch Changes - -- [#2763](https://github.com/udecode/plate/pull/2763) by [@12joan](https://github.com/12joan) – Update Zustood imports - -## 25.0.1 - -## 25.0.0 - -## 24.5.2 - -## 24.4.0 - -### Minor Changes - -- [#2675](https://github.com/udecode/plate/pull/2675) by [@zbeyens](https://github.com/zbeyens) – Support slate-react 0.99.0 - -## 24.3.6 - -## 24.3.5 - -## 24.3.2 - -## 24.3.1 - -## 24.3.0 - -## 24.2.0 - -## 24.0.2 - -## 24.0.1 - -## 24.0.0 - -## 23.7.4 - -## 23.7.0 - -## 23.6.0 - -## 23.3.1 - -## 23.3.0 - -## 22.0.2 - -## 22.0.1 - -## 22.0.0 - -### Minor Changes - -- [#2471](https://github.com/udecode/plate/pull/2471) by [@zbeyens](https://github.com/zbeyens) – New exports: - - `generateSrcAndSrcSet` - - `useCloudAttachmentElementState` - - `useCloudImageElementState` - -## 21.5.0 - -## 21.4.2 - -## 21.4.1 - -## 21.3.2 - -## 21.3.0 - -## 21.1.5 - -## 21.0.0 - -## 20.7.2 - -## 20.7.0 - -## 20.4.0 - -## 20.3.2 - -## 20.0.0 - -### Minor Changes - -- [#2254](https://github.com/udecode/plate/pull/2254) by [@thesunny](https://github.com/thesunny) – Add options to set `minResizeWidth` and `maxResizeWidth` to `CloudImagePlugin`. - - ```typescript - createCloudImagePlugin({ - options: { - maxInitialWidth: 320, - maxInitialHeight: 320, - minResizeWidth: 100, - maxResizeWidth: 720, - }, - }), - ``` - -## 19.7.0 - -## 19.5.0 - -## 19.4.4 - -## 19.4.2 - -## 19.2.0 - -## 19.1.1 - -## 19.1.0 - -## 19.0.3 - -## 19.0.1 - -## 19.0.0 - -## 18.15.0 - -## 18.13.0 - -### Patch Changes - -- [#1829](https://github.com/udecode/plate/pull/1829) by [@osamatanveer](https://github.com/osamatanveer) – - - vendor: remove `nanoid` from deps - -## 18.10.2 - -### Patch Changes - -- [#1995](https://github.com/udecode/plate/pull/1995) by [@zbeyens](https://github.com/zbeyens) – fix build - -## 18.10.1 - -### Patch Changes - -- [#1991](https://github.com/udecode/plate/pull/1991) by [@zbeyens](https://github.com/zbeyens) – fix - -## 18.10.0 - -### Minor Changes - -- [`2c3c403`](https://github.com/udecode/plate/commit/2c3c403bfab1063d590bb9d2476ba308f8390a44) by [@zbeyens](https://github.com/zbeyens) – Cloud plugins diff --git a/packages/cloud/README.md b/packages/cloud/README.md deleted file mode 100644 index 7021a665eb..0000000000 --- a/packages/cloud/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Plate Cloud Plugins - -This packages adds the official cloud plugin to provide upload services with support for attachments, images and image resizing. This is done in partnership with Portive. Includes server-side image resizing which delivers optimized images to each user. Supports high DPI files for retina devices and smaller files for faster delivery for non-high DPI devices. - -Includes: - -- Image uploads - - Resize images on server for faster downloads - - High DPI images for high DPI devices -- Attachment uploads - - Download attachments feature -- All uploads - - Drag and drop uploading - - Paste to upload - - Use a file picker to upload - - Upload progress bar - -## Documentation - -- Check out [Plate Cloud](https://platejs.org/docs/guides/cloud). - -## License - -[MIT](../../LICENSE) diff --git a/packages/cloud/package.json b/packages/cloud/package.json deleted file mode 100644 index a1417f9bb6..0000000000 --- a/packages/cloud/package.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "@udecode/plate-cloud", - "version": "41.0.0", - "description": "Plate plugin to add cloud based image and attachment uploads", - "keywords": [ - "plate", - "plugin", - "slate" - ], - "homepage": "https://platejs.org", - "bugs": { - "url": "https://github.com/udecode/plate/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/udecode/plate.git", - "directory": "packages/cloud" - }, - "license": "MIT", - "sideEffects": false, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "module": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "brl": "yarn p:brl", - "build": "yarn p:build", - "build:watch": "yarn p:build:watch", - "clean": "yarn p:clean", - "lint": "yarn p:lint", - "lint:fix": "yarn p:lint:fix", - "test": "yarn p:test", - "test:watch": "yarn p:test:watch", - "typecheck": "yarn p:typecheck" - }, - "dependencies": { - "@portive/client": "10.0.3", - "delay": "5.0.0", - "p-defer": "^4.0.1" - }, - "devDependencies": { - "@udecode/plate-common": "workspace:^" - }, - "peerDependencies": { - "@udecode/plate-common": ">=41.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "slate": ">=0.112.0", - "slate-dom": ">=0.111.0", - "slate-history": ">=0.93.0", - "slate-hyperscript": ">=0.66.0", - "slate-react": ">=0.111.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/cloud/src/attachment/CloudAttachmentPlugin.ts b/packages/cloud/src/attachment/CloudAttachmentPlugin.ts deleted file mode 100644 index 1f9d1a5e36..0000000000 --- a/packages/cloud/src/attachment/CloudAttachmentPlugin.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { insertNode } from '@udecode/plate-common'; -import { createPlatePlugin } from '@udecode/plate-common/react'; -import Defer from 'p-defer'; - -import type { UploadError, UploadSuccess } from '../upload'; -import type { TCloudAttachmentElement } from './types'; - -import { type FileEvent, type SuccessEvent, CloudPlugin } from '../cloud'; - -export const CloudAttachmentPlugin = createPlatePlugin({ - key: 'cloud_attachment', - dependencies: ['cloud'], - node: { isElement: true, isVoid: true }, -}).extendApi(({ editor }) => { - const { uploadStore } = editor.getOptions(CloudPlugin); - - const deferredFinish = Defer(); - const finishPromise = deferredFinish.promise; - - return { - onError(e: ErrorEvent & FileEvent) { - const upload: UploadError = { - message: e.message, - status: 'error', - url: e.url, - }; - uploadStore.set.upload(e.id, upload); - deferredFinish.resolve(upload); - }, - onProgress(e: FileEvent & ProgressEvent) { - uploadStore.set.upload(e.id, { - finishPromise, - sentBytes: (e as any).sentBytes, - status: 'progress', - totalBytes: (e as any).totalBytes, - url: e.url, - }); - }, - onStart(e: FileEvent) { - const node: TCloudAttachmentElement = { - bytes: e.file.size, - children: [{ text: '' }], - filename: e.file.name, - type: 'cloud_attachment', - url: e.id, - }; - - insertNode(editor, node); - - uploadStore.set.upload(e.id, { - finishPromise, - sentBytes: 0, - status: 'progress', - totalBytes: e.file.size, - url: e.url, - }); - }, - onSuccess(e: FileEvent & SuccessEvent) { - const upload: UploadSuccess = { - status: 'success', - url: e.url, - }; - uploadStore.set.upload(e.id, upload); - deferredFinish.resolve(upload); - }, - }; -}); diff --git a/packages/cloud/src/attachment/index.ts b/packages/cloud/src/attachment/index.ts deleted file mode 100644 index d7c0321522..0000000000 --- a/packages/cloud/src/attachment/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './CloudAttachmentPlugin'; -export * from './types'; -export * from './useCloudAttachmentElementState'; diff --git a/packages/cloud/src/attachment/types.ts b/packages/cloud/src/attachment/types.ts deleted file mode 100644 index db965958c1..0000000000 --- a/packages/cloud/src/attachment/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TElement } from '@udecode/plate-common'; - -/** Specifies just the `options` part of the CloudPlugin */ -export type CloudAttachmentPluginOptions = {}; - -export interface TCloudAttachmentElement extends TElement { - bytes: number; - filename: string; - url: string; -} diff --git a/packages/cloud/src/attachment/useCloudAttachmentElementState.ts b/packages/cloud/src/attachment/useCloudAttachmentElementState.ts deleted file mode 100644 index 8c32e5e0dc..0000000000 --- a/packages/cloud/src/attachment/useCloudAttachmentElementState.ts +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; - -import { isDefined, setNodes } from '@udecode/plate-common'; -import { findPath, useEditorRef } from '@udecode/plate-common/react'; -import { useFocused, useSelected } from 'slate-react'; - -import { type TCloudAttachmentElement, useUpload } from '..'; - -export const useCloudAttachmentElementState = ({ - element, -}: { - element: TCloudAttachmentElement; -}) => { - const editor = useEditorRef(); - - const upload = useUpload(element.url); - - const url = upload.status === 'not-found' ? undefined : upload.url; - - React.useEffect(() => { - /** - * We only want to update the actual URL of the element if the URL is not a - * blob URL and if it's different from the current URL. - * - * NOTE: - * - * If the user does an undo, this may cause some issues. The ideal solution - * is to change the URL once the upload is complete to the final URL and - * change the edit history so that the initial insertion of the cloud image - * appears to have the final URL. - */ - if (isDefined(url) && !url.startsWith('blob:') && url !== element.url) { - const path = findPath(editor, element); - setNodes( - editor, - { url }, - { - at: path, - } - ); - } - }, [editor, element, url]); - - const selected = useSelected(); - const focused = useFocused(); - - return { - focused, - selected, - upload, - }; -}; diff --git a/packages/cloud/src/cloud/CloudPlugin.ts b/packages/cloud/src/cloud/CloudPlugin.ts deleted file mode 100644 index 6f79f30ee3..0000000000 --- a/packages/cloud/src/cloud/CloudPlugin.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { PluginConfig, Value } from '@udecode/plate-common'; - -import * as portiveClient from '@portive/client'; -import { createTPlatePlugin } from '@udecode/plate-common/react'; - -import type { Upload } from '../upload'; -import type { FinishUploadsOptions } from './types'; - -import { createUploadStore } from '../upload/createUploadStore'; -import { finishUploads } from './finishUploads'; -import { getSaveValue } from './getSaveValue'; -import { onDropCloud, onPasteCloud } from './handlers'; -import { uploadFiles } from './uploadFiles'; - -export type CloudConfig = PluginConfig< - 'cloud', - { - client?: portiveClient.Client; - uploadStore?: ReturnType; - uploadStoreInitialValue?: Record; - } & portiveClient.ClientOptions, - { - cloud: { - finishUploads: (options?: FinishUploadsOptions) => Promise; - getSaveValue: () => Value; - uploadFiles: (files: Iterable) => void; - }; - } ->; - -export const CloudPlugin = createTPlatePlugin({ - key: 'cloud', - options: {}, - handlers: { - onDrop: ({ editor, event }) => onDropCloud(editor, event), - onPaste: ({ editor, event }) => onPasteCloud(editor, event), - }, -}) - .extend(({ editor, getOptions }) => { - let client: portiveClient.Client; - - const { apiKey, apiOrigin, authToken, uploadStoreInitialValue } = - getOptions(); - - try { - client = portiveClient.createClient({ apiKey, apiOrigin, authToken }); - } catch (error) { - editor.api.debug.error(error, 'PORTIVE_CLIENT'); - } - - return { - options: { - client: client!, - uploadStore: createUploadStore({ - uploads: uploadStoreInitialValue || {}, - }), - }, - }; - }) - .extendApi(({ editor, getOptions }) => { - return { - finishUploads: async (options?: FinishUploadsOptions): Promise => { - return finishUploads(editor, options); - }, - getSaveValue: (): Value => { - return getSaveValue( - editor.children, - getOptions().uploadStore.get.uploads() - ); - }, - uploadFiles: (files: Iterable) => { - uploadFiles(editor, files); - }, - }; - }); diff --git a/packages/cloud/src/cloud/finishUploads.ts b/packages/cloud/src/cloud/finishUploads.ts deleted file mode 100644 index 872fe2ab6e..0000000000 --- a/packages/cloud/src/cloud/finishUploads.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { SlateEditor } from '@udecode/plate-common'; - -import delay from 'delay'; - -import type { FinishUploadsOptions } from './types'; - -import { CloudPlugin } from './CloudPlugin'; -import { getInProgressUploads } from './getInProgressUploads'; - -const TEN_MINUTES = 1000 * 60 * 60; - -/** - * Finds all the in progress uploads and waits for them all to finish before - * resolving the returned promise. - * - * Optionally, provide a `maxTimeoutInMs` and if the timeout is reached, the - * method will return. This can be used if you only want to wait a certain - * amount of time. - */ -export const finishUploads = async ( - editor: SlateEditor, - { maxTimeoutInMs = TEN_MINUTES }: FinishUploadsOptions = {} -): Promise => { - const { uploadStore } = editor.getOptions(CloudPlugin); - - const uploads = uploadStore.get.uploads(); - const uploadingOrigins = getInProgressUploads(editor.children, uploads); - const finishPromises = uploadingOrigins.map((origin) => origin.finishPromise); - const timeoutPromise = delay(maxTimeoutInMs, { value: 'timeout' }); - await Promise.race([Promise.all(finishPromises), timeoutPromise]); -}; diff --git a/packages/cloud/src/cloud/generateSrcAndSrcSet.ts b/packages/cloud/src/cloud/generateSrcAndSrcSet.ts deleted file mode 100644 index 6909f602de..0000000000 --- a/packages/cloud/src/cloud/generateSrcAndSrcSet.ts +++ /dev/null @@ -1,61 +0,0 @@ -function generateSrc({ - maxSize, - size, - url, -}: { - maxSize: [number, number]; - size: [number, number]; - url: string; -}) { - /** If it's a url from `createObjectURL` then just return it */ - if (url.startsWith('blob:')) return url; - if (size[0] >= maxSize[0] || size[1] >= maxSize[1]) return url; - if (url.endsWith('.gif')) return url; - - return `${url}?size=${size[0]}x${size[1]}`; -} - -function generateSrcSet({ - maxSize, - size, - url, -}: { - maxSize: [number, number]; - size: [number, number]; - url: string; -}) { - /** If it's a url from `createObjectURL` then just return it */ - if (url.startsWith('blob:')) return url; - - const src1x = generateSrc({ - maxSize, - size, - url, - }); - const src2x = generateSrc({ - maxSize, - size: [size[0] * 2, size[1] * 2], - url, - }); - - return `${src1x}, ${src2x} 2x`; -} - -export function generateSrcAndSrcSet({ - maxSize, - size, - url, -}: { - maxSize: [number, number]; - size: [number, number]; - url?: string; -}) { - if (url === undefined) { - return { src: '', srcSet: '' }; - } - - const src = generateSrc({ maxSize, size, url }); - const srcSet = generateSrcSet({ maxSize, size, url }); - - return { src, srcSet }; -} diff --git a/packages/cloud/src/cloud/getInProgressUploads.ts b/packages/cloud/src/cloud/getInProgressUploads.ts deleted file mode 100644 index fc163a58ce..0000000000 --- a/packages/cloud/src/cloud/getInProgressUploads.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Descendant } from 'slate'; - -import type { Upload, UploadProgress } from '../upload/types'; - -import { isStoreRef } from './getSaveValue'; - -type MaybeUploadNode = { - [key: string]: unknown; - children?: MaybeUploadNode[]; - url?: unknown; -}; - -/** Recursive part of `normalizeOrigins` function with correct types. */ -const _getInProgressUploads = ( - nodes: MaybeUploadNode[], - uploads: Record, - progressUploads: UploadProgress[] -): UploadProgress[] => { - for (const node of nodes) { - if ( - 'url' in node && - /** - * If the `node` has a `url` then we either - * - * - Leave it alone and add it (it's already an actual URL) - * - If found in the store lookup, replace the store ref with a URL - * - If not found in lookup, skip it - */ typeof node.url === 'string' - ) { - /** If the `url` is a ref (i.e. starts with a `#`) */ - if (isStoreRef(node.url)) { - /** - * If the `url` is a reference to the `uploads` lookup Record, then we - * do a lookup. - * - * If found returns a value for the `upload` and the `status` is - * `complete`, then we swap out the reference with an actual `url`. - * - * If it's not found, we skip over it because we don't want it in our - * normalized value. - */ - const origin: Upload | undefined = uploads[node.url]; - - if (origin && origin.status === 'progress') { - progressUploads.push(origin); - } - } - - continue; - } - /** - * If there wasn't a `url` but there is `children`, then we iterate over the - * children to normalize them. - * - * For clarity, if there is both a `url` and `children`, the `children` - * won't be iterated over which is by design and a performance - * optimization. - */ - if (node.children) { - _getInProgressUploads(node.children, uploads, progressUploads); - - continue; - } - } - - return progressUploads; -}; - -/** - * Takes an array of `nodes` and a lookup for `origins` and normalizes the - * `nodes` such that: - * - * - Any node with an `id` that is a `url` is left alone - * - Any node with an `id` that is a `key` for lookup in `origins` is converted to - * a `url` if the origin file has been successfully uploaded - * - If the origin file has not been uploaded or is in an error state, then we - * remove that element. - * - * We do some typecasting here to help the Descendant values pass through. We - * are confident this is okay because we only augment the `id` and we only - * depend on the knowledge that `children`, if present, is an Array of nodes. - */ -export const getInProgressUploads = ( - nodes: Descendant[], - origins: Record -): UploadProgress[] => { - const progressUploads: UploadProgress[] = []; - - return _getInProgressUploads( - nodes as MaybeUploadNode[], - origins, - progressUploads - ); -}; diff --git a/packages/cloud/src/cloud/getSaveValue.ts b/packages/cloud/src/cloud/getSaveValue.ts deleted file mode 100644 index cb6df70298..0000000000 --- a/packages/cloud/src/cloud/getSaveValue.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { Value } from '@udecode/plate-common'; - -import type { Upload } from '../upload/types'; - -/** - * This is a local type that describes any Slate node we could possibly run - * across but specifying some properties that we are interested in checking. We - * don't put this in a `types.ts` file because it really only has use in this - * one method. - */ -type MaybeUploadNode = { - [key: string]: unknown; - children?: MaybeUploadNode[]; - url?: unknown; -}; - -/** - * Returns `true` only if the `url` passed in looks like it's not a real URL but - * rather a reference to be used to do a lookup in our uploads store. - */ -export const isStoreRef = (url: string) => url.startsWith('#'); - -/** Recursive part of `normalizeOrigins` function with correct types. */ -const _getSaveValue = ( - nodes: MaybeUploadNode[], - uploads: Record -): MaybeUploadNode[] => { - const nextNodes: MaybeUploadNode[] = []; - - for (const node of nodes) { - if ( - 'url' in node && - /** - * If the `node` has an `id` then we either - * - * - Leave it alone and add it (it's already normalized) - * - If found in lookup, replace the url and add it - * - If not found in lookup, skip it - */ typeof node.url === 'string' - ) { - /** If the `url` isn't a reference to a store then leave it as it is. */ - if (isStoreRef(node.url)) { - /** - * If the `url` is a key to the `uploads` lookup Record, then we do a - * lookup. - * - * If it returns a value for the `origin` and the `status` is - * `complete`, then we swap out the `id` with the `url`. - * - * If it's not found, we skip over it because we don't want it in our - * normalized value. - */ - const origin: Upload | undefined = uploads[node.url]; - - if (origin && origin.status === 'success') { - nextNodes.push({ ...node, url: origin.url }); - } - } else { - nextNodes.push(node); - } - - continue; - } - /** - * If there wasn't an `id` but there is `children`, then we iterate over the - * children to normalize them. - * - * For clarity, if there is both an `id` and `children`, the `children` - * won't be iterated over which is by design and a small performance - * optimization. - */ - if (node.children) { - nextNodes.push({ - ...node, - children: _getSaveValue(node.children, uploads), - }); - - continue; - } - - /** - * If there has been no prior processing of `id` or `children` then we - * simply add the node. - */ - nextNodes.push(node); - } - - return nextNodes; -}; - -/** - * Takes an array of `nodes` and a lookup for `origins` and normalizes the - * `nodes` such that: - * - * - Any node with an `id` that is a `url` is left alone - * - Any node with an `id` that is a `key` for lookup in `origins` is converted to - * a `url` if the origin file has been successfully uploaded - * - If the origin file has not been uploaded or is in an error state, then we - * remove that element. - * - * We do some typecasting here to help the Descendant values pass through. We - * are confident this is okay because we only augment the `id` and we only - * depend on the knowledge that `children`, if present, is an Array of nodes. - */ -export const getSaveValue = ( - nodes: V, - uploads: Record -): V => _getSaveValue(nodes as MaybeUploadNode[], uploads) as V; diff --git a/packages/cloud/src/cloud/handlers.ts b/packages/cloud/src/cloud/handlers.ts deleted file mode 100644 index 008d0b0a4f..0000000000 --- a/packages/cloud/src/cloud/handlers.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type React from 'react'; - -import type { SlateEditor } from '@udecode/plate-common'; - -import { select } from '@udecode/plate-common'; -import { findEventRange } from '@udecode/plate-common/react'; - -import { uploadFiles } from './uploadFiles'; - -export const onDropCloud = ( - editor: SlateEditor, - e: React.DragEvent -): boolean => { - const { files } = e.dataTransfer; - - if (files.length === 0) return false; - - /** Without this, the dropped file replaces the page */ - e.preventDefault(); - e.stopPropagation(); - /** - * When we drop a file, the selection won't move automatically to the drop - * location. Find the location from the event and upload the files at that - * location. - */ - const at = findEventRange(editor, e); - - if (!at) return false; - - select(editor, at); - uploadFiles(editor, files); - - return true; -}; - -export const onPasteCloud = ( - editor: SlateEditor, - e: React.ClipboardEvent -): boolean => { - const { files } = e.clipboardData; - - if (files.length === 0) return false; - - e.preventDefault(); - e.stopPropagation(); - uploadFiles(editor, files); - - return true; -}; diff --git a/packages/cloud/src/cloud/index.ts b/packages/cloud/src/cloud/index.ts deleted file mode 100644 index 91adcf0f3a..0000000000 --- a/packages/cloud/src/cloud/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './CloudPlugin'; -export * from './finishUploads'; -export * from './generateSrcAndSrcSet'; -export * from './getInProgressUploads'; -export * from './getSaveValue'; -export * from './handlers'; -export * from './types'; -export * from './uploadFiles'; diff --git a/packages/cloud/src/cloud/types.ts b/packages/cloud/src/cloud/types.ts deleted file mode 100644 index 587af2460f..0000000000 --- a/packages/cloud/src/cloud/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type * as portiveClient from '@portive/client'; -import type { Value } from '@udecode/plate-common'; - -import type { Upload } from '../upload'; -import type { createUploadStore } from '../upload/createUploadStore'; - -/** Specifies just the `options` part of the CloudPlugin */ -export type CloudPluginOptions = { - uploadStoreInitialValue?: Record; -} & portiveClient.ClientOptions; - -export type CloudPluginApi = { - cloud: { - client: portiveClient.Client; - finishUploads: (options?: FinishUploadsOptions) => Promise; - getSaveValue: () => Value; - uploadFiles: (msg: any) => void; - uploadStore: ReturnType; - // save: (options: { maxTimeoutInMs?: number }) => Promise; - }; -}; - -export type FinishUploadsOptions = { maxTimeoutInMs?: number }; - -/** - * The part of the FileEvent shared between the GenericFileEvent and the - * ImageFileEvent. - */ -export type FileEventBase = { - id: string; - file: File; - url: string; -}; - -/** FileEvent for files that are not images */ -export type GenericFileEvent = { - type: 'generic'; -} & FileEventBase; - -/** FileEvent for files that are images */ -export type ImageFileEvent = { - height: number; - type: 'image'; - width: number; -} & FileEventBase; - -/** FileEvent for any type of file (generic or image) */ -export type FileEvent = GenericFileEvent | ImageFileEvent; - -/** Indicates upload in progress */ -export type ProgressEvent = { - sentBytes: number; - totalBytes: number; -}; - -/** Indicates an error during upload */ -export type ErrorEvent = { - message: string; -}; - -/** Indicates a successful upload */ -export type SuccessEvent = { - url: string; -}; diff --git a/packages/cloud/src/cloud/uploadFiles.ts b/packages/cloud/src/cloud/uploadFiles.ts deleted file mode 100644 index a08c3bc12a..0000000000 --- a/packages/cloud/src/cloud/uploadFiles.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { SlateEditor } from '@udecode/plate-common'; - -import * as portiveClient from '@portive/client'; -import { nanoid } from '@udecode/plate-common'; - -import type { FileEvent, ProgressEvent } from './types'; - -import { CloudAttachmentPlugin } from '../attachment'; -import { CloudImagePlugin } from '../image'; -import { CloudPlugin } from './CloudPlugin'; - -const createFileEvent = ( - id: string, - clientFile: portiveClient.ClientFile -): FileEvent => { - if (clientFile.type === 'image') { - return { - id, - file: clientFile.file, - height: clientFile.height, - type: 'image', - url: clientFile.objectUrl, - width: clientFile.width, - }; - } - - return { - id, - file: clientFile.file, - type: 'generic', - url: clientFile.objectUrl, - }; -}; - -export const uploadFile = (editor: SlateEditor, file: File) => { - const { client } = editor.getOptions(CloudPlugin); - const apiImage = editor.getApi(CloudImagePlugin); - const apiAttachment = editor.getApi(CloudAttachmentPlugin); - - const id = `#${nanoid()}`; - - void portiveClient.uploadFile({ - client, - file, - onBeforeFetch(e) { - const fileEvent = createFileEvent(id, e.clientFile); - - if (fileEvent.type === 'image') { - apiImage.cloud_image?.onStart?.(fileEvent); - } else { - apiAttachment.cloud_attachment?.onStart?.(fileEvent); - } - }, - onError(e) { - const fileEvent = createFileEvent(id, e.clientFile); - - if (fileEvent.type === 'image') { - apiImage.cloud_image?.onError?.({ - ...fileEvent, - message: e.message, - }); - } else { - apiAttachment.cloud_attachment?.onError?.({ - ...fileEvent, - message: e.message, - } as any); - } - }, - onProgress(e) { - const fileEvent = createFileEvent(id, e.clientFile); - const progressEvent: ProgressEvent = { - sentBytes: e.sentBytes, - totalBytes: e.totalBytes, - }; - - if (fileEvent.type === 'image') { - apiImage.cloud_image?.onProgress?.({ - ...fileEvent, - ...progressEvent, - }); - } else { - apiAttachment.cloud_attachment?.onProgress?.({ - ...fileEvent, - ...progressEvent, - } as any); - } - }, - onSuccess(e) { - const fileEvent = createFileEvent(id, e.clientFile); - const { url } = e.hostedFile; - - if (fileEvent.type === 'image') { - apiImage.cloud_image?.onSuccess?.({ ...fileEvent, url }); - } else { - apiAttachment.cloud_attachment?.onSuccess?.({ - ...fileEvent, - url, - }); - } - }, - }); -}; - -export const uploadFiles = (editor: SlateEditor, files: Iterable) => { - for (const file of files) { - uploadFile(editor, file); - } -}; diff --git a/packages/cloud/src/image/CloudImagePlugin.ts b/packages/cloud/src/image/CloudImagePlugin.ts deleted file mode 100644 index cd1778df2f..0000000000 --- a/packages/cloud/src/image/CloudImagePlugin.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as portiveClient from '@portive/client'; -import { createPlatePlugin } from '@udecode/plate-common/react'; -import Defer from 'p-defer'; - -import type { UploadError, UploadSuccess } from '../upload'; -import type { TCloudImageElement } from './types'; - -import { - type ErrorEvent, - type ImageFileEvent, - type ProgressEvent, - type SuccessEvent, - CloudPlugin, -} from '../cloud'; - -export const CloudImagePlugin = createPlatePlugin({ - key: 'cloud_image', - dependencies: ['cloud'], - node: { isElement: true, isVoid: true }, - options: { - maxInitialHeight: 320, - maxInitialWidth: 320, - maxResizeWidth: 640, - minResizeWidth: 100, - }, -}).extendApi(({ editor, getOptions }) => { - const { uploadStore } = editor.getOptions(CloudPlugin); - - /** - * We create a deferredFinish which is an object with a `promise` and a way to - * `resolve` or `reject` the Promise outside of the Promise. We use `p-defer` - * library to do this. The `finish` Promise gets added to the `origin` object - * so we can await `origin.finish` during the save process to wait for all the - * files to finish uploading. - */ - const deferredFinish = Defer(); - const finishPromise = deferredFinish.promise; - - return { - onError: (e: ErrorEvent & ImageFileEvent) => { - const upload: UploadError = { - message: e.message, - status: 'error', - url: e.url, - }; - uploadStore.set.upload(e.id, upload); - deferredFinish.resolve(upload); - }, - onProgress: (e: ImageFileEvent & ProgressEvent) => { - uploadStore.set.upload(e.id, { - finishPromise, - sentBytes: e.sentBytes, - status: 'progress', - totalBytes: e.totalBytes, - url: e.url, - }); - }, - onStart: (e: ImageFileEvent) => { - const { height, width } = portiveClient.resizeIn( - { height: e.height, width: e.width }, - { - height: getOptions().maxInitialHeight, - width: getOptions().maxInitialWidth, - } - ); - const node: TCloudImageElement = { - bytes: e.file.size, - children: [{ text: '' }], - height, - maxHeight: e.height, - maxWidth: e.width, - type: 'cloud_image', - url: e.id, - width, - }; - editor.insertNode(node); - uploadStore.set.upload(e.id, { - finishPromise, - sentBytes: 0, - status: 'progress', - totalBytes: e.file.size, - url: e.url, - }); - }, - onSuccess: (e: ImageFileEvent & SuccessEvent) => { - const upload: UploadSuccess = { - status: 'success', - url: e.url, - }; - uploadStore.set.upload(e.id, upload); - deferredFinish.resolve(upload); - }, - }; -}); diff --git a/packages/cloud/src/image/index.ts b/packages/cloud/src/image/index.ts deleted file mode 100644 index eb7648ae10..0000000000 --- a/packages/cloud/src/image/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './CloudImagePlugin'; -export * from './types'; -export * from './useCloudImageElementState'; diff --git a/packages/cloud/src/image/types.ts b/packages/cloud/src/image/types.ts deleted file mode 100644 index 4ee1e13089..0000000000 --- a/packages/cloud/src/image/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TElement } from '@udecode/plate-common'; - -export interface TCloudImageElement extends TElement { - bytes: number; - height: number; - maxHeight: number; - maxWidth: number; - url: string; - width: number; -} diff --git a/packages/cloud/src/image/useCloudImageElementState.ts b/packages/cloud/src/image/useCloudImageElementState.ts deleted file mode 100644 index 5563a5b422..0000000000 --- a/packages/cloud/src/image/useCloudImageElementState.ts +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; - -import { setNodes } from '@udecode/plate-common'; -import { findPath, useEditorRef } from '@udecode/plate-common/react'; -import { useFocused, useSelected } from 'slate-react'; - -import { type TCloudImageElement, generateSrcAndSrcSet, useUpload } from '..'; - -export const useCloudImageElementState = ({ - element, -}: { - element: TCloudImageElement; -}) => { - const editor = useEditorRef(); - const upload = useUpload(element.url); - - const url = upload.status === 'not-found' ? undefined : upload.url; - - React.useEffect(() => { - /** - * We only want to update the actual URL of the element if the URL is not a - * blob URL and if it's different from the current URL. - * - * NOTE: - * - * If the user does an undo, this may cause some issues. The ideal solution - * is to change the URL once the upload is complete to the final URL and - * change the edit history so that the initial insertion of the cloud image - * appears to have the final URL. - */ - if ( - typeof url === 'string' && - !url.startsWith('blob:') && - url !== element.url - ) { - const path = findPath(editor, element); - setNodes( - editor, - { url }, - { - at: path, - } - ); - } - }, [editor, element, url]); - - const [size, setSize] = React.useState<{ height: number; width: number }>({ - height: element.height, - width: element.width, - }); - - React.useEffect(() => { - setSize({ height: element.height, width: element.width }); - }, [element.width, element.height]); - - const selected = useSelected(); - const focused = useFocused(); - - const { src, srcSet } = generateSrcAndSrcSet({ - maxSize: [element.maxWidth, element.maxHeight], - size: [element.width, element.height], - url: upload.status === 'not-found' ? undefined : upload.url, - }); - - return { - focused, - selected, - setSize, - size, - src, - srcSet, - upload, - }; -}; diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts deleted file mode 100644 index 117f38cb03..0000000000 --- a/packages/cloud/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './attachment/index'; -export * from './cloud/index'; -export * from './image/index'; -export * from './upload/index'; diff --git a/packages/cloud/src/upload/createUploadStore.ts b/packages/cloud/src/upload/createUploadStore.ts deleted file mode 100644 index 2064e26719..0000000000 --- a/packages/cloud/src/upload/createUploadStore.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createZustandStore } from '@udecode/plate-common'; - -import type { Upload } from './types'; - -/** - * Creates an origin store using `zustood`. - * - * The purpose of this is to keep track of uploads and their progress but only - * storing the key to the lookup in the Element itself. We do it this way - * because we don't want to modify the Editor value during the upload or it - * becomes part of the edit history. - */ -export const createUploadStore = ({ - uploads: initialUploads = {}, -}: { - uploads?: Record; -} = {}) => { - return createZustandStore('upload')({ uploads: initialUploads }) - .extendActions((set, get) => ({ - upload: (id: string, upload: Upload): void => { - const uploads = get.uploads(); - set.uploads({ ...uploads, [id]: upload }); - }, - })) - .extendSelectors((state, get) => ({ - upload: (id: string): Upload | undefined => { - const uploads = get.uploads(); - - return uploads[id]; - }, - })); -}; diff --git a/packages/cloud/src/upload/index.ts b/packages/cloud/src/upload/index.ts deleted file mode 100644 index 624540102e..0000000000 --- a/packages/cloud/src/upload/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from './createUploadStore'; -export * from './types'; -export * from './useUpload'; diff --git a/packages/cloud/src/upload/types.ts b/packages/cloud/src/upload/types.ts deleted file mode 100644 index 0600095146..0000000000 --- a/packages/cloud/src/upload/types.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** Indicates an `Upload` that is uploading and the state of the Upload */ -export type UploadProgress = { - finishPromise: Promise; - sentBytes: number; - status: 'progress'; - totalBytes: number; - url: string; -}; - -/** Indicates an `Upload` that has completed uploading */ -export type UploadSuccess = { - status: 'success'; - url: string; -}; - -/** - * Indicates an `Upload` that has an error during uploading and the Error - * message - */ -export type UploadError = { - message: string; - status: 'error'; - url: string; -}; - -/** Indicated the `Upload` could not be found. */ -export type UploadStateNotFound = { - status: 'not-found'; - // no url here -}; - -export type Upload = - | UploadError - | UploadProgress - | UploadStateNotFound - | UploadSuccess; - -/** - * `UploadState` - * - * Types related to the `zustand` state-management library which we use to store - * the state of uploads. - */ - -export type GetUpload = (id: string) => Upload; - -export type SetUpload = (id: string, upload: Upload) => void; - -export type UploadState = { - getUpload: GetUpload; - setUpload: SetUpload; - uploads: Record; -}; diff --git a/packages/cloud/src/upload/useUpload.ts b/packages/cloud/src/upload/useUpload.ts deleted file mode 100644 index 7d5780a296..0000000000 --- a/packages/cloud/src/upload/useUpload.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEditorRef } from '@udecode/plate-common/react'; - -import type { Upload } from './types'; - -import { CloudPlugin } from '../cloud'; - -/** - * Takes an `element` (which it only needs for its `id`) and returns the Upload - * object from it. - */ -export const useUpload = (id: string): Upload => { - const editor = useEditorRef(); - - const { uploadStore } = editor.getOptions(CloudPlugin); - - /** - * We call this even if it's not always required because it calls `useStore` - * which is a React hook which means it needs to be called consistently. - */ - // const upload: Upload = editor.cloud.useUploadStore( - // (state) => state.uploads[id] || { status: 'not-found' } - // ); - const upload: Upload = uploadStore.use.upload(id) || { - status: 'not-found', - }; - - // ( - // (state) => state.uploads[id] || { status: 'not-found' } - // ); - if (id.includes('/')) { - return { - status: 'success', - url: id, - }; - } - - return upload; -}; diff --git a/packages/cloud/tsconfig.build.json b/packages/cloud/tsconfig.build.json deleted file mode 100644 index 425481e027..0000000000 --- a/packages/cloud/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../config/tsconfig.build.json", - "compilerOptions": { - "declarationDir": "./dist", - "outDir": "./dist" - }, - "include": ["src"] -} diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json deleted file mode 100644 index ad83d092a5..0000000000 --- a/packages/cloud/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "include": ["src"], - "exclude": [] -} From 2fc0103c79a34999324b89d6aa5266c1855f39b1 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 02:29:17 +0100 Subject: [PATCH 07/12] fix --- packages/layout/src/lib/withColumn.ts | 86 ++++++++++++++++++--------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/packages/layout/src/lib/withColumn.ts b/packages/layout/src/lib/withColumn.ts index 0a0450fdaf..962c3183d9 100644 --- a/packages/layout/src/lib/withColumn.ts +++ b/packages/layout/src/lib/withColumn.ts @@ -5,6 +5,7 @@ import { isElement, isStartPoint, removeNodes, + setNodes, unwrapNodes, } from '@udecode/plate-common'; @@ -43,40 +44,37 @@ export const withColumn: ExtendEditor = ({ editor }) => { return; } - } - - // const prevChildrenCnt = node.children.length; - // const currentLayout = node.layout; - - // if (currentLayout) { - // const currentChildrenCnt = currentLayout.length; - - // const groupPathRef = createPathRef(editor, path); - - // if (prevChildrenCnt === 2 && currentChildrenCnt === 3) { - // const lastChildPath = getLastChildPath(entry); - // insertColumn(editor, { - // at: lastChildPath, - // }); + // PERF: only run when the number of columns changes + editor.withoutNormalizing(() => { + // Add new width normalization logic + const totalColumns = node.children.length; + let widths = node.children.map((col) => { + const parsed = Number.parseFloat(col.width); - // setColumnWidth(editor, groupPathRef, currentLayout); + return Number.isNaN(parsed) ? 0 : parsed; + }); - // return; - // } - // if (prevChildrenCnt === 3 && currentChildrenCnt === 2) { - // moveMiddleColumn(editor, entry, { direction: 'left' }); - // setColumnWidth(editor, groupPathRef, currentLayout); + const sum = widths.reduce((acc, w) => acc + w, 0); - // return; - // } - // if (prevChildrenCnt === currentChildrenCnt) { - // setColumnWidth(editor, groupPathRef, currentLayout); + if (sum !== 100) { + const diff = 100 - sum; + const adjustment = diff / totalColumns; - // return; - // } - // } + widths = widths.map((w) => w + adjustment); + // Update the columns with the new widths + widths.forEach((w, i) => { + const columnPath = path.concat([i]); + setNodes( + editor, + { width: `${w}%` }, + { at: columnPath } + ); + }); + } + }); + } // If it's a column, ensure it has at least one block (optional) if (isElement(n) && n.type === BaseColumnItemPlugin.key) { const node = n as TColumnElement; @@ -113,3 +111,35 @@ export const withColumn: ExtendEditor = ({ editor }) => { return editor; }; + +// const prevChildrenCnt = node.children.length; +// const currentLayout = node.layout; + +// if (currentLayout) { +// const currentChildrenCnt = currentLayout.length; + +// const groupPathRef = createPathRef(editor, path); + +// if (prevChildrenCnt === 2 && currentChildrenCnt === 3) { +// const lastChildPath = getLastChildPath(entry); + +// insertColumn(editor, { +// at: lastChildPath, +// }); + +// setColumnWidth(editor, groupPathRef, currentLayout); + +// return; +// } +// if (prevChildrenCnt === 3 && currentChildrenCnt === 2) { +// moveMiddleColumn(editor, entry, { direction: 'left' }); +// setColumnWidth(editor, groupPathRef, currentLayout); + +// return; +// } +// if (prevChildrenCnt === currentChildrenCnt) { +// setColumnWidth(editor, groupPathRef, currentLayout); + +// return; +// } +// } From 34bcc834fcdf909c12e62fcd28761732eef6cb4f Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 02:39:21 +0100 Subject: [PATCH 08/12] fix --- .../src/react/hooks/useEditableProps.spec.tsx | 2 +- .../layout/src/lib/transforms/setColumns.spec.tsx | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/core/src/react/hooks/useEditableProps.spec.tsx b/packages/core/src/react/hooks/useEditableProps.spec.tsx index a401998d12..866b5ab150 100644 --- a/packages/core/src/react/hooks/useEditableProps.spec.tsx +++ b/packages/core/src/react/hooks/useEditableProps.spec.tsx @@ -30,7 +30,7 @@ describe('useEditableProps', () => { ); - expect(decorate).toHaveBeenCalledTimes(3); + expect(decorate).toHaveBeenCalledTimes(4); }); }); diff --git a/packages/layout/src/lib/transforms/setColumns.spec.tsx b/packages/layout/src/lib/transforms/setColumns.spec.tsx index a851c72dee..fb2f32169a 100644 --- a/packages/layout/src/lib/transforms/setColumns.spec.tsx +++ b/packages/layout/src/lib/transforms/setColumns.spec.tsx @@ -62,11 +62,11 @@ describe('setColumns', () => { expect(node.children).toHaveLength(3); // First two columns updated - expect(node.children[0].width).toBe('33%'); - expect(node.children[1].width).toBe('33%'); + expect(node.children[0].width).toContain('33.'); + expect(node.children[1].width).toContain('33.'); // New column inserted - expect(node.children[2].width).toBe('33%'); + expect(node.children[2].width).toContain('33.'); // Should have a default block inside expect(node.children[2].children).toHaveLength(1); expect(node.children[2].children[0].type).toBe('p'); @@ -173,9 +173,8 @@ describe('setColumns', () => { const node = editor.children[0] as any; expect(node.children).toHaveLength(2); - // Even though this sums to 80%, we do not enforce total width - expect(node.children[0].width).toBe('40%'); - expect(node.children[1].width).toBe('40%'); + expect(node.children[0].width).toBe('50%'); + expect(node.children[1].width).toBe('50%'); }); it('should handle multiple toggles without losing content', () => { @@ -205,7 +204,7 @@ describe('setColumns', () => { expect(node.children).toHaveLength(2); // Col3 content should have merged into column 2 expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); - expect(node.children[1].children[1].children[0].text).toBe('Column 3 text'); + expect(node.children[1].children[2].children[0].text).toBe('Column 3 text'); // Toggle again to 3 columns setColumns(editor, { at: columnGroupPath, widths: ['33%', '33%', '34%'] }); @@ -216,7 +215,7 @@ describe('setColumns', () => { expect(node.children[2].children.length).toBeGreaterThan(0); // Original content is still preserved in columns 2 expect(node.children[1].children[0].children[0].text).toBe('Column 2 text'); - expect(node.children[1].children[1].children[0].text).toBe('Column 3 text'); + expect(node.children[1].children[2].children[0].text).toBe('Column 3 text'); expect(node.children[2].children[0].children[0].text).toBe(''); }); From 677bc9461762b44cd6ec968508e62dd3b7c9690f Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 03:10:06 +0100 Subject: [PATCH 09/12] vendor --- apps/www/package.json | 1 - config/aliases.js | 1 - yarn.lock | 101 +----------------------------------------- 3 files changed, 1 insertion(+), 102 deletions(-) diff --git a/apps/www/package.json b/apps/www/package.json index 04da1bb936..75b0ce9077 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -84,7 +84,6 @@ "@udecode/plate-block-quote": "workspace:^", "@udecode/plate-break": "workspace:^", "@udecode/plate-caption": "workspace:^", - "@udecode/plate-cloud": "workspace:^", "@udecode/plate-code-block": "workspace:^", "@udecode/plate-combobox": "workspace:^", "@udecode/plate-comments": "workspace:^", diff --git a/config/aliases.js b/config/aliases.js index 84c6772292..4c1fe431aa 100644 --- a/config/aliases.js +++ b/config/aliases.js @@ -6,7 +6,6 @@ module.exports = { '@udecode/plate-basic-marks': 'basic-marks', '@udecode/plate-block-quote': 'block-quote', '@udecode/plate-break': 'break', - '@udecode/plate-cloud': 'cloud', '@udecode/plate-code-block': 'code-block', '@udecode/plate-combobox': 'combobox', '@udecode/plate-comments': 'comments', diff --git a/yarn.lock b/yarn.lock index ef44f0ea08..6e2726fbe2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3209,27 +3209,6 @@ __metadata: languageName: node linkType: hard -"@portive/api-types@npm:^9.0.0": - version: 9.0.0 - resolution: "@portive/api-types@npm:9.0.0" - dependencies: - "@thesunny/assert-type": "npm:^0.1.13" - superstruct: "npm:^0.15.4" - checksum: 10c0/7055f09e497450ba40dc3b841677a52743c805aba5a89c08748fdb3d8bb6a22ba819714a76cbe46f86e3ef8c3ce49b82ca1708b294d24f60bc8aabf56b3491b2 - languageName: node - linkType: hard - -"@portive/client@npm:10.0.3": - version: 10.0.3 - resolution: "@portive/client@npm:10.0.3" - dependencies: - "@portive/api-types": "npm:^9.0.0" - axios: "npm:^0.27.2" - resolvable-value: "npm:^1.0.2" - checksum: 10c0/9e998f3f9e8b3b8475c843fd5ec123c96f9f0952941ec3a18bffe4477cfac585e17a45258c4ec313ebdcf88f74cca5c78435bfef77ac342ec412fb5b3a2618c2 - languageName: node - linkType: hard - "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -5444,15 +5423,6 @@ __metadata: languageName: node linkType: hard -"@thesunny/assert-type@npm:^0.1.13": - version: 0.1.13 - resolution: "@thesunny/assert-type@npm:0.1.13" - dependencies: - ts-node: "npm:^10.9.1" - checksum: 10c0/059e420ad9013cd7b9983c9300449efa620fa40975bfd34da2ce43a757f8f30991091eac309057b82b5c9436183bf5fbfcfcf32fa869b6a79e52087916d15dcd - languageName: node - linkType: hard - "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -6557,26 +6527,6 @@ __metadata: languageName: unknown linkType: soft -"@udecode/plate-cloud@workspace:^, @udecode/plate-cloud@workspace:packages/cloud": - version: 0.0.0-use.local - resolution: "@udecode/plate-cloud@workspace:packages/cloud" - dependencies: - "@portive/client": "npm:10.0.3" - "@udecode/plate-common": "workspace:^" - delay: "npm:5.0.0" - p-defer: "npm:^4.0.1" - peerDependencies: - "@udecode/plate-common": ">=41.0.0" - react: ">=16.8.0" - react-dom: ">=16.8.0" - slate: ">=0.112.0" - slate-dom: ">=0.111.0" - slate-history: ">=0.93.0" - slate-hyperscript: ">=0.66.0" - slate-react: ">=0.111.0" - languageName: unknown - linkType: soft - "@udecode/plate-code-block@npm:41.0.0, @udecode/plate-code-block@workspace:^, @udecode/plate-code-block@workspace:packages/code-block": version: 0.0.0-use.local resolution: "@udecode/plate-code-block@workspace:packages/code-block" @@ -8124,16 +8074,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^0.27.2": - version: 0.27.2 - resolution: "axios@npm:0.27.2" - dependencies: - follow-redirects: "npm:^1.14.9" - form-data: "npm:^4.0.0" - checksum: 10c0/76d673d2a90629944b44d6f345f01e58e9174690f635115d5ffd4aca495d99bcd8f95c590d5ccb473513f5ebc1d1a6e8934580d0c57cdd0498c3a101313ef771 - languageName: node - linkType: hard - "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -9587,13 +9527,6 @@ __metadata: languageName: node linkType: hard -"delay@npm:5.0.0": - version: 5.0.0 - resolution: "delay@npm:5.0.0" - checksum: 10c0/01cdc4cd0cd35fb622518a3df848e67e09763a38e7cdada2232b6fda9ddda72eddcf74f0e24211200fbe718434f2335f2a2633875a6c96037fefa6de42896ad7 - languageName: node - linkType: hard - "delay@npm:^6.0.0": version: 6.0.0 resolution: "delay@npm:6.0.0" @@ -11696,16 +11629,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.14.9": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f - languageName: node - linkType: hard - "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -16825,13 +16748,6 @@ __metadata: languageName: node linkType: hard -"p-defer@npm:^4.0.1": - version: 4.0.1 - resolution: "p-defer@npm:4.0.1" - checksum: 10c0/592f5bd32f8c6a57f892b00976e5272b3bbbd792b503f4cf3bc22094d08d7a973413c59c15deccff4759d860b38467a08b5b3363e865da6f00f44a031777118c - languageName: node - linkType: hard - "p-filter@npm:^2.1.0": version: 2.1.0 resolution: "p-filter@npm:2.1.0" @@ -18969,13 +18885,6 @@ __metadata: languageName: node linkType: hard -"resolvable-value@npm:^1.0.2": - version: 1.0.2 - resolution: "resolvable-value@npm:1.0.2" - checksum: 10c0/e3141df461131d77c31b6bea9044be4f53b25688decb79a4b21f6d6f9028f32833756128039ad28826c7c09497192542ba79480522c244efb9e232aa1352d620 - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -20405,13 +20314,6 @@ __metadata: languageName: node linkType: hard -"superstruct@npm:^0.15.4": - version: 0.15.5 - resolution: "superstruct@npm:0.15.5" - checksum: 10c0/73ae2043443dcc7868da6e8b4e4895410c79a88e021b514c665161199675ee920d5eadd85bb9dee5a9f515817e62f4b65a67ccb82d29f73259d012afcbcd3ce4 - languageName: node - linkType: hard - "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -20969,7 +20871,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.9.1, ts-node@npm:^10.9.2": +"ts-node@npm:^10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: @@ -22485,7 +22387,6 @@ __metadata: "@udecode/plate-block-quote": "workspace:^" "@udecode/plate-break": "workspace:^" "@udecode/plate-caption": "workspace:^" - "@udecode/plate-cloud": "workspace:^" "@udecode/plate-code-block": "workspace:^" "@udecode/plate-combobox": "workspace:^" "@udecode/plate-comments": "workspace:^" From 698dbff712d120835fe26e577f5343a0bbcd6947 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 03:10:13 +0100 Subject: [PATCH 10/12] fix --- .../registry/default/plate-ui/draggable.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/www/src/registry/default/plate-ui/draggable.tsx b/apps/www/src/registry/default/plate-ui/draggable.tsx index 96951824bd..928a3e7c4b 100644 --- a/apps/www/src/registry/default/plate-ui/draggable.tsx +++ b/apps/www/src/registry/default/plate-ui/draggable.tsx @@ -15,6 +15,7 @@ import { useEditorPlugin, useEditorRef, useElement, + usePath, } from '@udecode/plate-common/react'; import { useDraggable, useDropLine } from '@udecode/plate-dnd'; import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react'; @@ -95,9 +96,12 @@ export const DraggableAboveNodes: NodeWrapperComponent = (props) => { export const Draggable = withRef<'div', PlateRenderElementProps>( ({ className, ...props }, ref) => { - const { children, editor, element } = props; + const { children, editor, element, path } = props; const { isDragging, previewRef, handleRef } = useDraggable({ element }); + const isInColumn = path?.length === 3; + const isInTable = path?.length === 4; + return (
( HEADING_KEYS.h3, HEADING_KEYS.h4, HEADING_KEYS.h5, - ]) && 'h-[1.3em]' + ]) && 'h-[1.3em]', + isInColumn && 'h-4', + isInTable && 'mt-1 size-4' )} >
@@ -153,11 +160,15 @@ const Gutter = React.forwardRef< >(({ children, className, ...props }, ref) => { const { editor, useOption } = useEditorPlugin(BlockSelectionPlugin); const element = useElement(); + const path = usePath(); const isSelectionAreaVisible = useOption('isSelectionAreaVisible'); const selected = useSelected(); const isNodeType = (keys: string[] | string) => isType(editor, element, keys); + const isInColumn = path?.length === 3; + const isInTable = path?.length === 4; + return (
Date: Fri, 20 Dec 2024 03:10:17 +0100 Subject: [PATCH 11/12] docs --- apps/www/content/docs/api/core.mdx | 43 +++++++++++++++++++++++++++++ apps/www/content/docs/api/utils.mdx | 11 +++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/apps/www/content/docs/api/core.mdx b/apps/www/content/docs/api/core.mdx index 985d573cf1..9202b20a15 100644 --- a/apps/www/content/docs/api/core.mdx +++ b/apps/www/content/docs/api/core.mdx @@ -405,6 +405,49 @@ Returns a prop value derived from the current selection fragment. A value derived from the fragment nodes, or undefined if no consistent value is found across the specified nodes. +### useNodePath + +Returns the path of a node in the editor. + + + + The node to find the path for. + + + + + A memoized Path array representing the location of the node in the editor's tree structure. + + +### usePath + +Get the memoized path of the closest element. + + + + The key of the plugin to get the path for. + + + + + The path of the element, or `undefined` if used outside of a node component's context. + + +### useElement + +Get the element by plugin key. + + + + The key of the plugin to get the element for. + - **Default:** `'element'` + + + + + The element of type `T extends TElement`, or an empty object if used outside of a node component's context. + + ## Core plugins ### DebugPlugin diff --git a/apps/www/content/docs/api/utils.mdx b/apps/www/content/docs/api/utils.mdx index c6d32c03b5..76df95c484 100644 --- a/apps/www/content/docs/api/utils.mdx +++ b/apps/www/content/docs/api/utils.mdx @@ -18,14 +18,17 @@ PlateElementProps. The CSS class to apply to the component. - -The editor instance. - Additional props to pass to the component. + +The editor instance. Also available using `useEditorRef` hook. + -The element node. +The element node. Also available using `useElement` hook. + + +The path of the element in the editor tree. Also available using `usePath` hook. Attributes of the element to be spread on the top-level element. From e20c8a2d3ab7fad987549f763ec107980a8fa2ca Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 04:01:01 +0100 Subject: [PATCH 12/12] fix --- packages/plate-utils/src/react/PlateElement.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/plate-utils/src/react/PlateElement.tsx b/packages/plate-utils/src/react/PlateElement.tsx index 4f1f0b418e..fbcccabe31 100644 --- a/packages/plate-utils/src/react/PlateElement.tsx +++ b/packages/plate-utils/src/react/PlateElement.tsx @@ -20,8 +20,14 @@ export type PlateElementProps< PlateRenderElementProps; export const usePlateElement = (props: PlateElementProps) => { - const { attributes, element, elementToAttributes, nodeProps, ...rootProps } = - omitPluginContext(props); + const { + attributes, + element, + elementToAttributes, + nodeProps, + path, + ...rootProps + } = omitPluginContext(props); const mounted = usePlateStore().get.isMounted(); const block = React.useMemo(