From 8e4fcbb0b74bf01296500f6f412f7cda6905363c Mon Sep 17 00:00:00 2001 From: zbeyens Date: Fri, 20 Dec 2024 01:28:50 +0100 Subject: [PATCH] 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": [] }