diff --git a/apps/www/src/app/(app)/_components/installation-tab.tsx b/apps/www/src/app/(app)/_components/installation-tab.tsx index be1bbd2137..f4e22a5853 100644 --- a/apps/www/src/app/(app)/_components/installation-tab.tsx +++ b/apps/www/src/app/(app)/_components/installation-tab.tsx @@ -309,6 +309,7 @@ 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 = [ @@ -317,11 +318,11 @@ export default function InstallationTab() { pluginsCode.join('\n'), ' ],', ' override: {', - ` components: ${hasPlaceholder ? 'withPlaceholders(' : ''}({`, + ` components: ${hasDraggable ? 'withDraggables(' : ''}${hasPlaceholder ? 'withPlaceholders(' : ''}({`, ...componentsWithPluginKey.map( ({ pluginKey, usage }) => ` [${pluginKey}]: ${usage},` ), - ` })${hasPlaceholder ? ')' : ''},`, + ` })${hasPlaceholder ? ')' : ''}${hasDraggable ? ')' : ''},`, ' },', ' value: [', ' {', diff --git a/apps/www/src/config/customizer-items.ts b/apps/www/src/config/customizer-items.ts index aaf77be80d..b1d23434a4 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 441b42ddd1..95cf5f8276 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,56 +92,59 @@ 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: 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: 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, + }) + ), }, plugins: [ ...copilotPlugins, diff --git a/apps/www/src/registry/default/block/slate-to-html/page.tsx b/apps/www/src/registry/default/block/slate-to-html/page.tsx index 36acdfae45..2edcc5ae73 100644 --- a/apps/www/src/registry/default/block/slate-to-html/page.tsx +++ b/apps/www/src/registry/default/block/slate-to-html/page.tsx @@ -60,6 +60,7 @@ import { BaseTableRowPlugin, } from '@udecode/plate-table'; import { BaseTogglePlugin } from '@udecode/plate-toggle'; +import { cookies } from 'next/headers'; import fs from 'node:fs/promises'; import path from 'node:path'; import Prism from 'prismjs'; @@ -128,6 +129,8 @@ import { TableRowElementStatic } from '@/registry/default/plate-ui/table-row-ele import { TocElementStatic } from '@/registry/default/plate-ui/toc-element-static'; import { ToggleElementStatic } from '@/registry/default/plate-ui/toggle-element-static'; +export const dynamic = 'force-dynamic'; + export const description = 'Slate to HTML'; export const iframeHeight = '800px'; @@ -300,9 +303,8 @@ export default async function SlateToHtmlBlock() { const tailwindCss = await getCachedTailwindCss(); const prismCss = await getCachedPrismCss(); - // const cookieStore = await cookies(); - // const theme = cookieStore.get('theme')?.value; - const theme = 'light'; + const cookieStore = await cookies(); + const theme = cookieStore.get('theme')?.value; // Get the editor content HTML using EditorStatic const editorHtml = await serializeHtml(editor, { 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 e8b3b35dca..f5569c0f58 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 @@ -1,217 +1,8 @@ 'use client'; -import type { NodeWrapperComponent } from '@udecode/plate-common/react'; - -import { BlockquotePlugin } from '@udecode/plate-block-quote/react'; -import { CodeBlockPlugin } from '@udecode/plate-code-block/react'; -import { findNode } from '@udecode/plate-common'; -import { ParagraphPlugin } from '@udecode/plate-common/react'; -import { DndPlugin, useWithDraggable } from '@udecode/plate-dnd'; -import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react'; -import { HEADING_KEYS, HEADING_LEVELS } from '@udecode/plate-heading'; -import { ColumnPlugin } from '@udecode/plate-layout/react'; -import { - ImagePlugin, - MediaEmbedPlugin, - PlaceholderPlugin, -} from '@udecode/plate-media/react'; +import { DndPlugin } from '@udecode/plate-dnd'; +import { PlaceholderPlugin } from '@udecode/plate-media/react'; import { NodeIdPlugin } from '@udecode/plate-node-id'; -import { TablePlugin } from '@udecode/plate-table/react'; -import { TogglePlugin } from '@udecode/plate-toggle/react'; - -import { Draggable } from '@/registry/default/plate-ui/draggable'; - -const draggableKeys = [ - ParagraphPlugin.key, - 'ul', - 'ol', - ...HEADING_LEVELS, - BlockquotePlugin.key, - CodeBlockPlugin.key, - ImagePlugin.key, - MediaEmbedPlugin.key, - ExcalidrawPlugin.key, - TogglePlugin.key, - ColumnPlugin.key, - PlaceholderPlugin.key, - TablePlugin.key, -]; - -const options = [ - { - keys: draggableKeys, - filter: (editor, path) => { - if (path.length === 1) { - return false; - } - - const block = findNode(editor, { - at: path, - match: { - type: [editor.getType(ColumnPlugin), editor.getType(TablePlugin)], - }, - }); - - if (block?.[0].type === editor.getType(TablePlugin)) { - return path.length !== 4; - } - if (block?.[0].type === editor.getType(ColumnPlugin)) { - return path.length !== 3; - } - - return true; - }, - level: null, - }, - { - 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', - }, - }, - { - keys: draggableKeys, - draggableProps: { - className: '[&_.slate-gutterLeft]:w-3', - }, - filter: (editor, path) => { - if (path.length === 1) { - return false; - } - - const block = findNode(editor, { - at: path, - match: { - type: [editor.getType(ColumnPlugin), editor.getType(TablePlugin)], - }, - }); - - if (block?.[0].type === editor.getType(TablePlugin)) { - return path.length !== 4; - } - if (block?.[0].type === editor.getType(ColumnPlugin)) { - return path.length !== 3; - } - - return true; - }, - level: null, - }, -]; - -const RenderDraggableAboveNodes: NodeWrapperComponent = () => { - const { disabled, draggableProps } = useWithDraggable({ - ...options, - // ...props, - }); - - console.log({ disabled, draggableProps }); - - if (disabled) return; - - return ({ children, ...props }) => ( - - - - ); -}; export const dndPlugins = [ NodeIdPlugin, @@ -224,8 +15,5 @@ export const dndPlugins = [ .insert.media(dragItem.files, { at: target, nextBlock: false }); }, }, - render: { - aboveNodes: renderDraggableAboveNodes, - }, }), ] 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 bd1448eca8..26d7431155 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: string[] = [ +export const STRUCTURAL_TYPES = [ ColumnPlugin.key, ColumnItemPlugin.key, TablePlugin.key, @@ -79,7 +79,7 @@ const insertBlockMap: Record< (editor: PlateEditor, type: string) => void > = { [ACTION_THREE_COLUMNS]: (editor) => - insertColumnGroup(editor, { columns: 3, select: true }), + insertColumnGroup(editor, { layout: 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, { columns: 3 }), + [ACTION_THREE_COLUMNS]: (editor) => toggleColumnGroup(editor, { layout: 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 acacdacadc..1b90c14313 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,55 +88,58 @@ 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: 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: 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' }), + }) + ), }, 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 f3820bcff5..d452926a5c 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,6 +91,7 @@ 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'; @@ -161,7 +162,9 @@ export const useCreateEditor = ( { override: { components: { - ...(readOnly ? viewComponents : withPlaceholders(editorComponents)), + ...(readOnly + ? viewComponents + : withPlaceholders(withDraggables(editorComponents))), ...components, }, ...override, diff --git a/apps/www/src/registry/default/plate-ui/column-group-element.tsx b/apps/www/src/registry/default/plate-ui/column-group-element.tsx index 1e8fb6aff2..17293165dd 100644 --- a/apps/www/src/registry/default/plate-ui/column-group-element.tsx +++ b/apps/www/src/registry/default/plate-ui/column-group-element.tsx @@ -2,23 +2,15 @@ import React from 'react'; +import type { TColumnElement } from '@udecode/plate-layout'; + import { cn, withRef } from '@udecode/cn'; -import { - useEditorRef, - useElement, - useRemoveNodeButton, -} from '@udecode/plate-common/react'; -import { - type TColumnElement, - type TColumnGroupElement, - setColumns, -} from '@udecode/plate-layout'; +import { useElement, useRemoveNodeButton } from '@udecode/plate-common/react'; import { ColumnItemPlugin, - ColumnPlugin, + useColumnState, useDebouncePopoverOpen, } from '@udecode/plate-layout/react'; -import { findNodePath } from '@udecode/slate'; import { type LucideProps, Trash2Icon } from 'lucide-react'; import { useReadOnly } from 'slate-react'; @@ -40,23 +32,22 @@ export const ColumnGroupElement = withRef( ); 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 ( @@ -70,38 +61,26 @@ export function ColumnFloatingToolbar({ children }: React.PropsWithChildren) { sideOffset={10} >
- - - diff --git a/apps/www/src/registry/default/plate-ui/draggable.tsx b/apps/www/src/registry/default/plate-ui/draggable.tsx index d08c0fbe7d..ec514ce65c 100644 --- a/apps/www/src/registry/default/plate-ui/draggable.tsx +++ b/apps/www/src/registry/default/plate-ui/draggable.tsx @@ -12,7 +12,6 @@ import { MemoizedChildren, useEditorPlugin, useEditorRef, - useElement, } from '@udecode/plate-common/react'; import { type DragItemNode, @@ -25,8 +24,6 @@ import { BlockSelectionPlugin } from '@udecode/plate-selection/react'; import { GripVertical } from 'lucide-react'; import { useSelected } from 'slate-react'; -import { STRUCTURAL_TYPES } from '@/registry/default/components/editor/transforms'; - import { Tooltip, TooltipContent, @@ -66,9 +63,7 @@ export const Draggable = withRef<'div', DraggableProps>( className={cn( 'relative', isDragging && 'opacity-50', - STRUCTURAL_TYPES.includes(element.type) - ? 'group/structural' - : 'group', + 'group', className )} > @@ -102,7 +97,6 @@ const Gutter = React.forwardRef< React.HTMLAttributes >(({ children, className, ...props }, ref) => { const { useOption } = useEditorPlugin(BlockSelectionPlugin); - const element = useElement(); const isSelectionAreaVisible = useOption('isSelectionAreaVisible'); const gutter = useDraggableGutter(); const selected = useSelected(); @@ -112,10 +106,7 @@ const Gutter = React.forwardRef< ref={ref} className={cn( 'slate-gutterLeft', - 'absolute -top-px z-50 flex h-full -translate-x-full cursor-text hover:opacity-100 sm:opacity-0', - STRUCTURAL_TYPES.includes(element.type) - ? 'main-hover:group-hover/structural:opacity-100' - : 'main-hover:group-hover:opacity-100', + 'absolute -top-px z-50 flex h-full -translate-x-full cursor-text hover:opacity-100 sm:opacity-0 main-hover:group-hover:opacity-100', isSelectionAreaVisible && 'hidden', !selected && 'opacity-0', className diff --git a/apps/www/src/registry/default/plate-ui/table-cell-element-static.tsx b/apps/www/src/registry/default/plate-ui/table-cell-element-static.tsx index f24382a23b..0040d0b9fa 100644 --- a/apps/www/src/registry/default/plate-ui/table-cell-element-static.tsx +++ b/apps/www/src/registry/default/plate-ui/table-cell-element-static.tsx @@ -61,7 +61,7 @@ export function TableCellElementStatic({ } {...props} > -
+
{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 abd0f7e7a3..b0bacd0987 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 4052c3ef38..edbbbbd9b1 100644 --- a/apps/www/src/registry/registry-ui.ts +++ b/apps/www/src/registry/registry-ui.ts @@ -250,7 +250,9 @@ 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 { HTML5Backend } from 'react-dnd-html5-backend'; + +import { withDraggables } from './withDraggables';`, `export function MyEditor() { const editor = usePlateEditor({ plugins: [ @@ -259,9 +261,9 @@ import { HTML5Backend } from 'react-dnd-html5-backend';`, DndPlugin.configure({ options: { enableScroller: true } }), ], override: { - components: { + components: withDraggables({ // ...components - }, + }), } }); diff --git a/packages/layout/src/lib/index.ts b/packages/layout/src/lib/index.ts index 14119d4244..34b1e779d8 100644 --- a/packages/layout/src/lib/index.ts +++ b/packages/layout/src/lib/index.ts @@ -6,4 +6,3 @@ 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 2a5fee6fed..5075b4c4a2 100644 --- a/packages/layout/src/lib/transforms/index.ts +++ b/packages/layout/src/lib/transforms/index.ts @@ -5,6 +5,5 @@ export * from './insertColumn'; export * from './insertColumnGroup'; export * from './moveMiddleColumn'; -export * from './resizeColumn'; -export * from './setColumns'; +export * from './setColumnWidth'; export * from './toggleColumnGroup'; diff --git a/packages/layout/src/lib/transforms/insertColumnGroup.ts b/packages/layout/src/lib/transforms/insertColumnGroup.ts index bd81d110ca..8f108769c2 100644 --- a/packages/layout/src/lib/transforms/insertColumnGroup.ts +++ b/packages/layout/src/lib/transforms/insertColumnGroup.ts @@ -14,26 +14,27 @@ import { BaseColumnItemPlugin, BaseColumnPlugin } from '../BaseColumnPlugin'; export const insertColumnGroup = ( editor: SlateEditor, { - columns = 2, + layout = 2, select: selectProp, ...options }: InsertNodesOptions & { - columns?: number; + layout?: number[] | number; } = {} ) => { - const width = 100 / columns; + const columnLayout = Array.isArray(layout) + ? layout + : Array(layout).fill(Math.floor(100 / layout)); withoutNormalizing(editor, () => { insertNodes( editor, { - children: Array(columns) - .fill(null) - .map(() => ({ - children: [editor.api.create.block()], - type: BaseColumnItemPlugin.key, - width: `${width}%`, - })), + children: columnLayout.map((width) => ({ + children: [editor.api.create.block()], + type: BaseColumnItemPlugin.key, + width: `${width}%`, + })), + layout: columnLayout, type: BaseColumnPlugin.key, }, options diff --git a/packages/layout/src/lib/transforms/moveMiddleColumn.ts b/packages/layout/src/lib/transforms/moveMiddleColumn.ts index 117253e33e..5ab1b2fff6 100644 --- a/packages/layout/src/lib/transforms/moveMiddleColumn.ts +++ b/packages/layout/src/lib/transforms/moveMiddleColumn.ts @@ -2,19 +2,17 @@ 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 if direction is 'left', or to the right if - * 'right'. If the middle node is empty, return false and remove it. + * Move the middle column to the left of right by options.direction. if the + * middle node is empty return false and remove it. */ export const moveMiddleColumn = ( editor: SlateEditor, @@ -28,12 +26,8 @@ export const moveMiddleColumn = ( if (direction === 'left') { const DESCENDANT_PATH = [1]; - const middleChildNode = getNode(node, DESCENDANT_PATH); - - if (!middleChildNode) return false; - - // Check emptiness using Node.string - const isEmpty = getNodeString(middleChildNode) === ''; + const middleChildNode = Node.get(node, DESCENDANT_PATH); + const isEmpty = editor.isEmpty(middleChildNode as any); const middleChildPathRef = editor.pathRef(path.concat(DESCENDANT_PATH)); @@ -43,9 +37,7 @@ export const moveMiddleColumn = ( return false; } - const firstNode = getNodeDescendant(node, [0]); - - if (!firstNode) return false; + const firstNode = Node.descendant(node, [0]) as TColumnElement; 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 deleted file mode 100644 index 39c3623ecc..0000000000 --- a/packages/layout/src/lib/transforms/resizeColumn.ts +++ /dev/null @@ -1,66 +0,0 @@ -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 new file mode 100644 index 0000000000..f60abd4b82 --- /dev/null +++ b/packages/layout/src/lib/transforms/setColumnWidth.ts @@ -0,0 +1,44 @@ +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 deleted file mode 100644 index a851c72dee..0000000000 --- a/packages/layout/src/lib/transforms/setColumns.spec.tsx +++ /dev/null @@ -1,348 +0,0 @@ -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 deleted file mode 100644 index 5ba2898e4f..0000000000 --- a/packages/layout/src/lib/transforms/setColumns.ts +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index acdd11764e..0000000000 --- a/packages/layout/src/lib/transforms/toggleColumnGroup.spec.tsx +++ /dev/null @@ -1,183 +0,0 @@ -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 a5bc388795..671cbfc85f 100644 --- a/packages/layout/src/lib/transforms/toggleColumnGroup.ts +++ b/packages/layout/src/lib/transforms/toggleColumnGroup.ts @@ -9,54 +9,40 @@ 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, - columns = 2, - widths, + layout = 2, }: Partial, 'nodes'>> & { - columns?: number; - widths?: string[]; + layout?: number[] | number; } = {} ) => { const entry = getBlockAbove(editor, { at }); - const columnGroupEntry = getBlockAbove(editor, { - at, - match: { type: editor.getType(BaseColumnPlugin) }, - }); if (!entry) return; - 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]))); - } + 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]))); }; diff --git a/packages/layout/src/lib/types.ts b/packages/layout/src/lib/types.ts index b4a757b5c9..4b391e159f 100644 --- a/packages/layout/src/lib/types.ts +++ b/packages/layout/src/lib/types.ts @@ -10,4 +10,5 @@ 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 deleted file mode 100644 index db9e1e415b..0000000000 --- a/packages/layout/src/lib/utils/columnsToWidths.ts +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 0a70ca33f6..0000000000 --- a/packages/layout/src/lib/utils/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @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 0a0450fdaf..1c864e56d8 100644 --- a/packages/layout/src/lib/withColumn.ts +++ b/packages/layout/src/lib/withColumn.ts @@ -1,6 +1,8 @@ import { type ExtendEditor, + createPathRef, getAboveNode, + getLastChildPath, isCollapsed, isElement, isStartPoint, @@ -11,18 +13,17 @@ 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, normalizeNode } = editor; + const { deleteBackward, isEmpty, 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) => @@ -34,7 +35,6 @@ 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,41 +43,39 @@ export const withColumn: ExtendEditor = ({ editor }) => { return; } - } - - // const prevChildrenCnt = node.children.length; - // const currentLayout = node.layout; - // if (currentLayout) { - // const currentChildrenCnt = currentLayout.length; + const prevChildrenCnt = node.children.length; + const currentLayout = node.layout; - // const groupPathRef = createPathRef(editor, path); + if (currentLayout) { + const currentChildrenCnt = currentLayout.length; - // if (prevChildrenCnt === 2 && currentChildrenCnt === 3) { - // const lastChildPath = getLastChildPath(entry); + const groupPathRef = createPathRef(editor, path); - // insertColumn(editor, { - // at: lastChildPath, - // }); + if (prevChildrenCnt === 2 && currentChildrenCnt === 3) { + const lastChildPath = getLastChildPath(entry); - // setColumnWidth(editor, groupPathRef, currentLayout); + insertColumn(editor, { + at: lastChildPath, + }); - // return; - // } - // if (prevChildrenCnt === 3 && currentChildrenCnt === 2) { - // moveMiddleColumn(editor, entry, { direction: 'left' }); - // setColumnWidth(editor, groupPathRef, currentLayout); + setColumnWidth(editor, groupPathRef, currentLayout); - // return; - // } - // if (prevChildrenCnt === currentChildrenCnt) { - // setColumnWidth(editor, groupPathRef, currentLayout); + return; + } + if (prevChildrenCnt === 3 && currentChildrenCnt === 2) { + moveMiddleColumn(editor, entry, { direction: 'left' }); + setColumnWidth(editor, groupPathRef, currentLayout); - // return; - // } - // } + return; + } + if (prevChildrenCnt === currentChildrenCnt) { + setColumnWidth(editor, groupPathRef, currentLayout); - // If it's a column, ensure it has at least one block (optional) + return; + } + } + } if (isElement(n) && n.type === BaseColumnItemPlugin.key) { const node = n as TColumnElement; @@ -111,5 +109,13 @@ 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 new file mode 100644 index 0000000000..5cdbb13831 --- /dev/null +++ b/packages/layout/src/react/column-store.ts @@ -0,0 +1,46 @@ +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 757a6f621e..f9190c9f6e 100644 --- a/packages/layout/src/react/index.ts +++ b/packages/layout/src/react/index.ts @@ -3,5 +3,6 @@ */ export * from './ColumnPlugin'; +export * from './column-store'; export * from './onKeyDownColumn'; export * from './hooks/index';