From e6920290aaa4868d254bd2399a4f1af92c991130 Mon Sep 17 00:00:00 2001 From: Aodhagan Murphy Date: Mon, 8 Jul 2024 17:02:12 +0100 Subject: [PATCH 1/3] feat: enabling lists instead table cells [] --- packages/rich-text/src/Toolbar/index.tsx | 3 ++- .../src/plugins/List/transforms/unwrapList.ts | 21 ++++++++++++++++--- .../src/plugins/Table/createTablePlugin.ts | 7 ++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/rich-text/src/Toolbar/index.tsx b/packages/rich-text/src/Toolbar/index.tsx index fb3233606..fbf25bdfb 100644 --- a/packages/rich-text/src/Toolbar/index.tsx +++ b/packages/rich-text/src/Toolbar/index.tsx @@ -119,6 +119,7 @@ const Toolbar = ({ isDisabled }: ToolbarProps) => { const sdk = useSdkContext(); const editor = useContentfulEditor(); const canInsertBlocks = !isNodeTypeSelected(editor, BLOCKS.TABLE); + const canInsertListBlocks = !isNodeTypeSelected(editor, BLOCKS.TABLE_HEADER_CELL); const validationInfo = React.useMemo(() => getValidationInfo(sdk.field), [sdk.field]); const isListSelected = isNodeTypeSelected(editor, BLOCKS.UL_LIST) || isNodeTypeSelected(editor, BLOCKS.OL_LIST); @@ -183,7 +184,7 @@ const Toolbar = ({ isDisabled }: ToolbarProps) => { {validationInfo.isAnyBlockFormattingEnabled && } - + {isNodeTypeEnabled(sdk.field, BLOCKS.QUOTE) && ( diff --git a/packages/rich-text/src/plugins/List/transforms/unwrapList.ts b/packages/rich-text/src/plugins/List/transforms/unwrapList.ts index 334f46fd1..04569bacc 100644 --- a/packages/rich-text/src/plugins/List/transforms/unwrapList.ts +++ b/packages/rich-text/src/plugins/List/transforms/unwrapList.ts @@ -9,13 +9,28 @@ import { getNodeEntries, isElement } from '../../../internal/queries'; import { unwrapNodes, liftNodes } from '../../../internal/transforms'; import { PlateEditor, Path } from '../../../internal/types'; -function hasUnliftedListItems(editor: PlateEditor, at?: Path) { +function hasUnliftedListItems(editor: PlateEditor, stoppingIndex: number, at?: Path) { return getNodeEntries(editor, { at, - match: (node, path) => isElement(node) && node.type === BLOCKS.LIST_ITEM && path.length >= 2, + match: (node, path) => + isElement(node) && node.type === BLOCKS.LIST_ITEM && path.length >= stoppingIndex, }).next().done; } + +function getStoppingIndex(editor: PlateEditor, at?: Path) { + const tableCell = getNodeEntries(editor, { + at, + match: (node) => { + return isElement(node) && node.type === BLOCKS.TABLE_CELL; + }, + }).next().value; + const rootStoppingIndex = 2; + return tableCell ? tableCell[1].length + rootStoppingIndex : rootStoppingIndex; +} + export const unwrapList = (editor: PlateEditor, { at }: { at?: Path } = {}) => { + const stoppingIndex = getStoppingIndex(editor, at); + withoutNormalizing(editor, () => { do { // lift list items to the root level @@ -24,7 +39,7 @@ export const unwrapList = (editor: PlateEditor, { at }: { at?: Path } = {}) => { match: (node) => isElement(node) && node.type === BLOCKS.LIST_ITEM, mode: 'lowest', }); - } while (!hasUnliftedListItems(editor, at)); + } while (!hasUnliftedListItems(editor, stoppingIndex, at)); // finally unwrap all lifted items unwrapNodes(editor, { diff --git a/packages/rich-text/src/plugins/Table/createTablePlugin.ts b/packages/rich-text/src/plugins/Table/createTablePlugin.ts index 57d66924c..d68a7452e 100644 --- a/packages/rich-text/src/plugins/Table/createTablePlugin.ts +++ b/packages/rich-text/src/plugins/Table/createTablePlugin.ts @@ -143,7 +143,12 @@ export const createTablePlugin = (): PlatePlugin => component: Cell, normalizer: [ { - validChildren: CONTAINERS[BLOCKS.TABLE_CELL], + validChildren: [ + ...CONTAINERS[BLOCKS.TABLE_CELL], + ...CONTAINERS[BLOCKS.UL_LIST], + ...CONTAINERS[BLOCKS.OL_LIST], + ...CONTAINERS[BLOCKS.LIST_ITEM], + ], transform: withInvalidCellChildrenTracking(transformParagraphs), }, ], From 34326c3001128bdebfe6b9d94c866aa266234f68 Mon Sep 17 00:00:00 2001 From: Aodhagan Murphy Date: Mon, 8 Jul 2024 22:03:22 +0100 Subject: [PATCH 2/3] feat: removing unwrap list override [] --- .../src/plugins/List/transforms/toggleList.ts | 3 +- .../src/plugins/List/transforms/unwrapList.ts | 51 ------------------- 2 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 packages/rich-text/src/plugins/List/transforms/unwrapList.ts diff --git a/packages/rich-text/src/plugins/List/transforms/toggleList.ts b/packages/rich-text/src/plugins/List/transforms/toggleList.ts index 95adbf9d1..3debc2df0 100644 --- a/packages/rich-text/src/plugins/List/transforms/toggleList.ts +++ b/packages/rich-text/src/plugins/List/transforms/toggleList.ts @@ -4,7 +4,7 @@ */ import { BLOCKS } from '@contentful/rich-text-types'; import { ELEMENT_LIC } from '@udecode/plate-list'; -import { getListItemEntry } from '@udecode/plate-list'; +import { getListItemEntry, unwrapList } from '@udecode/plate-list'; import { withoutNormalizing } from '../../../internal'; import { ELEMENT_DEFAULT } from '../../../internal/constants'; @@ -22,7 +22,6 @@ import { } from '../../../internal/queries'; import { setNodes, wrapNodes } from '../../../internal/transforms'; import { PlateEditor, Element, Location, NodeEntry } from '../../../internal/types'; -import { unwrapList } from './unwrapList'; const listTypes = [BLOCKS.UL_LIST, BLOCKS.OL_LIST] as string[]; diff --git a/packages/rich-text/src/plugins/List/transforms/unwrapList.ts b/packages/rich-text/src/plugins/List/transforms/unwrapList.ts deleted file mode 100644 index 04569bacc..000000000 --- a/packages/rich-text/src/plugins/List/transforms/unwrapList.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Credit: Modified version of Plate's list plugin - * See: https://github.com/udecode/plate/blob/main/packages/nodes/list - */ -import { BLOCKS } from '@contentful/rich-text-types'; - -import { withoutNormalizing } from '../../../internal'; -import { getNodeEntries, isElement } from '../../../internal/queries'; -import { unwrapNodes, liftNodes } from '../../../internal/transforms'; -import { PlateEditor, Path } from '../../../internal/types'; - -function hasUnliftedListItems(editor: PlateEditor, stoppingIndex: number, at?: Path) { - return getNodeEntries(editor, { - at, - match: (node, path) => - isElement(node) && node.type === BLOCKS.LIST_ITEM && path.length >= stoppingIndex, - }).next().done; -} - -function getStoppingIndex(editor: PlateEditor, at?: Path) { - const tableCell = getNodeEntries(editor, { - at, - match: (node) => { - return isElement(node) && node.type === BLOCKS.TABLE_CELL; - }, - }).next().value; - const rootStoppingIndex = 2; - return tableCell ? tableCell[1].length + rootStoppingIndex : rootStoppingIndex; -} - -export const unwrapList = (editor: PlateEditor, { at }: { at?: Path } = {}) => { - const stoppingIndex = getStoppingIndex(editor, at); - - withoutNormalizing(editor, () => { - do { - // lift list items to the root level - liftNodes(editor, { - at, - match: (node) => isElement(node) && node.type === BLOCKS.LIST_ITEM, - mode: 'lowest', - }); - } while (!hasUnliftedListItems(editor, stoppingIndex, at)); - - // finally unwrap all lifted items - unwrapNodes(editor, { - at, - match: { type: BLOCKS.LIST_ITEM }, - split: false, - }); - }); -}; From 0b83a1a6207ba95bbfd391b5982e98f4a0f8b233 Mon Sep 17 00:00:00 2001 From: Aodhagan Murphy Date: Mon, 8 Jul 2024 23:56:17 +0100 Subject: [PATCH 3/3] feat: reintroducing unwrap logic with special handling for embeds [] --- packages/rich-text/src/internal/queries.ts | 24 ++--- packages/rich-text/src/internal/transforms.ts | 24 +++-- .../src/plugins/List/transforms/toggleList.ts | 3 +- .../src/plugins/List/transforms/unwrapList.ts | 97 +++++++++++++++++++ 4 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 packages/rich-text/src/plugins/List/transforms/unwrapList.ts diff --git a/packages/rich-text/src/internal/queries.ts b/packages/rich-text/src/internal/queries.ts index 73bd7a18f..e615de548 100644 --- a/packages/rich-text/src/internal/queries.ts +++ b/packages/rich-text/src/internal/queries.ts @@ -44,14 +44,14 @@ export const isNode = (value: unknown): value is Node => { export const isSelectionAtBlockEnd = ( editor: PlateEditor, - options?: p.GetAboveNodeOptions, + options?: p.GetAboveNodeOptions ) => { return p.isSelectionAtBlockEnd(editor, options); }; export const isSelectionAtBlockStart = ( editor: PlateEditor, - options?: p.GetAboveNodeOptions, + options?: p.GetAboveNodeOptions ) => { return p.isSelectionAtBlockStart(editor, options); }; @@ -72,7 +72,7 @@ export const getNodeEntries = (editor: PlateEditor, options?: p.GetNodeEntriesOp export const getNodeChildren = ( root: Ancestor, path: Path, - options?: s.NodeChildrenOptions | undefined, + options?: s.NodeChildrenOptions | undefined ) => { return p.getNodeChildren(root, path, options); }; @@ -80,7 +80,7 @@ export const getNodeChildren = ( export const getParentNode = ( editor: PlateEditor, at: Location, - options?: s.EditorParentOptions, + options?: s.EditorParentOptions ) => { return p.getParentNode(editor, at, options) as NodeEntry | undefined; }; @@ -109,7 +109,7 @@ export const getDescendantNodeByPath = (root: Node, path: s.Path): Node => { export const getNodeDescendants = ( root: PlateEditor | Node, - options?: s.NodeDescendantsOptions, + options?: s.NodeDescendantsOptions ) => { return p.getNodeDescendants(root, { ...options, pass: undefined }); }; @@ -123,7 +123,7 @@ export const isRangeAcrossBlocks = ( editor: p.TEditor, options?: | (Omit, 'at'> & { at?: s.BaseRange | null | undefined }) - | undefined, + | undefined ) => { return p.isRangeAcrossBlocks(editor, options); }; @@ -159,7 +159,7 @@ export const getNextNode = (editor: PlateEditor, opts?: p.GetNextNodeOptions { return p.getCommonNode(root, path, another); }; @@ -171,7 +171,7 @@ export const getNodeTexts = ( to?: s.Path; pass?: (ne: NodeEntry) => boolean; reverse?: boolean; - }, + } ) => { return p.getNodeTexts(root, opts); }; @@ -258,7 +258,7 @@ export const matchNode = (node: Node, path: s.Path, fn: p.Predicate boolean, + predicate: (node: HTMLElement) => boolean ) => { return p.someHtmlElement(rootNode, predicate); }; @@ -266,7 +266,7 @@ export const someHtmlElement = ( export const getPointBefore = ( editor: PlateEditor, at: Location, - options?: s.EditorBeforeOptions, + options?: s.EditorBeforeOptions ) => { return p.getPointBefore(editor, at, options); }; @@ -274,7 +274,7 @@ export const getPointBefore = ( export const getPointAfter = ( editor: PlateEditor, at: Location, - options?: s.EditorAfterOptions, + options?: s.EditorAfterOptions ) => { return p.getPointAfter(editor, at, options); }; @@ -282,7 +282,7 @@ export const getPointAfter = ( export const isEndPoint = ( editor: PlateEditor, point: BasePoint | null | undefined, - at: Location, + at: Location ) => { return p.isEndPoint(editor, point, at); }; diff --git a/packages/rich-text/src/internal/transforms.ts b/packages/rich-text/src/internal/transforms.ts index 936195be9..38b879748 100644 --- a/packages/rich-text/src/internal/transforms.ts +++ b/packages/rich-text/src/internal/transforms.ts @@ -25,7 +25,7 @@ import { */ export const normalize = ( editor: PlateEditor, - options: s.EditorNormalizeOptions = { force: true }, + options: s.EditorNormalizeOptions = { force: true } ) => { return p.normalizeEditor(editor, options); }; @@ -60,7 +60,7 @@ export const collapseSelection = (editor: PlateEditor, options?: SelectionCollap export const setNodes = ( editor: PlateEditor, attrs: Partial>, - opts?: p.SetNodesOptions, + opts?: p.SetNodesOptions ) => { p.setNodes(editor, attrs, opts); }; @@ -68,7 +68,7 @@ export const setNodes = ( export const unsetNodes = ( editor: PlateEditor, props: string | number | (string | number)[], - options?: p.UnsetNodesOptions | undefined, + options?: p.UnsetNodesOptions | undefined ) => { p.unsetNodes(editor, props, options); }; @@ -76,7 +76,7 @@ export const unsetNodes = ( export const insertNodes = ( editor: PlateEditor, nodes: Node | Node[], - opts?: p.InsertNodesOptions, + opts?: p.InsertNodesOptions ) => { return p.insertNodes(editor, nodes, opts); }; @@ -96,7 +96,7 @@ export const unwrapNodes = (editor: PlateEditor, options?: p.UnwrapNodesOptions< export const wrapNodes = ( editor: PlateEditor, element: Element, - options?: p.WrapNodesOptions, + options?: p.WrapNodesOptions ) => { return p.wrapNodes(editor, element, options); }; @@ -104,7 +104,7 @@ export const wrapNodes = ( export const toggleNodeType = ( editor: PlateEditor, options: ToggleNodeTypeOptions, - editorOptions?: Omit, + editorOptions?: Omit ) => { p.toggleNodeType(editor, options, editorOptions); }; @@ -116,7 +116,7 @@ export const removeMark = (editor: PlateEditor, type: string, at: BaseRange) => export const unhangRange = ( editor: PlateEditor, range?: Path | BasePoint | BaseRange | Span | null | undefined, - options?: p.UnhangRangeOptions | undefined, + options?: p.UnhangRangeOptions | undefined ) => { return p.unhangRange(editor, range, options); }; @@ -147,7 +147,7 @@ export const moveNodes = (editor: PlateEditor, opts?: p.MoveNodesOptions) export const deleteFragment = ( editor: PlateEditor, - options?: s.EditorFragmentDeletionOptions | undefined, + options?: s.EditorFragmentDeletionOptions | undefined ) => { return p.deleteFragment(editor, options); }; @@ -178,3 +178,11 @@ export const setEditorValue = (editor: PlateEditor, nodes?: Node[]): void => { } }); }; + +export const setElements = ( + editor: PlateEditor, + props: Partial>, + options?: p.SetNodesOptions +) => { + p.setElements(editor, props, options); +}; diff --git a/packages/rich-text/src/plugins/List/transforms/toggleList.ts b/packages/rich-text/src/plugins/List/transforms/toggleList.ts index 3debc2df0..95adbf9d1 100644 --- a/packages/rich-text/src/plugins/List/transforms/toggleList.ts +++ b/packages/rich-text/src/plugins/List/transforms/toggleList.ts @@ -4,7 +4,7 @@ */ import { BLOCKS } from '@contentful/rich-text-types'; import { ELEMENT_LIC } from '@udecode/plate-list'; -import { getListItemEntry, unwrapList } from '@udecode/plate-list'; +import { getListItemEntry } from '@udecode/plate-list'; import { withoutNormalizing } from '../../../internal'; import { ELEMENT_DEFAULT } from '../../../internal/constants'; @@ -22,6 +22,7 @@ import { } from '../../../internal/queries'; import { setNodes, wrapNodes } from '../../../internal/transforms'; import { PlateEditor, Element, Location, NodeEntry } from '../../../internal/types'; +import { unwrapList } from './unwrapList'; const listTypes = [BLOCKS.UL_LIST, BLOCKS.OL_LIST] as string[]; diff --git a/packages/rich-text/src/plugins/List/transforms/unwrapList.ts b/packages/rich-text/src/plugins/List/transforms/unwrapList.ts new file mode 100644 index 000000000..a01a6829d --- /dev/null +++ b/packages/rich-text/src/plugins/List/transforms/unwrapList.ts @@ -0,0 +1,97 @@ +/** + * Credit: Modified version of Plate's list plugin + * See: https://github.com/udecode/plate/blob/main/packages/list/src/transforms/unwrapList.ts + */ + +import { BLOCKS } from '@contentful/rich-text-types'; +import { getListTypes } from '@udecode/plate-list'; + +import { + getAboveNode, + getBlockAbove, + getCommonNode, + getPluginType, + isElement, +} from '../../../internal/queries'; +import { withoutNormalizing, unwrapNodes, setElements } from '../../../internal/transforms'; +import { PlateEditor, Path, Descendant } from '../../../internal/types'; + +function handleEmbeddedType( + editor: PlateEditor, + blockType: BLOCKS.EMBEDDED_ASSET | BLOCKS.EMBEDDED_ENTRY, + at?: Path, + firstChild?: Descendant +) { + if (firstChild) { + setElements(editor, { + at, + type: getPluginType(editor, blockType), + value: { ...firstChild }, + }); + } +} + +export const unwrapList = (editor: PlateEditor, { at }: { at?: Path } = {}) => { + const ancestorListTypeCheck = () => { + if (getAboveNode(editor, { match: { at, type: getListTypes(editor) } })) { + return true; + } + // The selection's common node might be a list type + if (!at && editor.selection) { + const commonNode = getCommonNode( + editor, + editor.selection.anchor.path, + editor.selection.focus.path + ); + + if (isElement(commonNode[0]) && getListTypes(editor).includes(commonNode[0].type)) { + return true; + } + } + + return false; + }; + + withoutNormalizing(editor, () => { + do { + const licEntry = getBlockAbove(editor, { + at, + match: { type: getPluginType(editor, BLOCKS.LIST_ITEM) }, + }); + + if (licEntry) { + // Special case for embedded entry and asset + // if we don't do these they will get replaced by a paragraph and lose + // their value. + const firstChild = licEntry[0]?.children[0]; + if ( + firstChild.type === BLOCKS.EMBEDDED_ENTRY || + firstChild.type === BLOCKS.EMBEDDED_ASSET + ) { + handleEmbeddedType(editor, firstChild.type, at, firstChild); + } else { + setElements(editor, { + at, + type: getPluginType(editor, BLOCKS.PARAGRAPH), + }); + } + } + + unwrapNodes(editor, { + at, + match: { type: getPluginType(editor, BLOCKS.LIST_ITEM) }, + // in original code split is set to true + // ommited here as we get an extra list item when switching off list blocks + }); + + unwrapNodes(editor, { + at, + match: { + type: [getPluginType(editor, BLOCKS.UL_LIST), getPluginType(editor, BLOCKS.OL_LIST)], + }, + // in original code split is set to true + // ommited here as we get an extra list item when switching off list blocks + }); + } while (ancestorListTypeCheck()); + }); +};