diff --git a/src/admin/components/forms/field-types/RichText/elements/ListButton.tsx b/src/admin/components/forms/field-types/RichText/elements/ListButton.tsx index 081fad0a86..1ab56b8fc1 100644 --- a/src/admin/components/forms/field-types/RichText/elements/ListButton.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/ListButton.tsx @@ -1,10 +1,10 @@ import React, { useCallback } from 'react'; import { useSlate } from 'slate-react'; -import isListActive from './isListActive'; import toggleList from './toggleList'; import { ButtonProps } from './types'; import '../buttons.scss'; +import isListActive from './isListActive'; export const baseClass = 'rich-text__button'; diff --git a/src/admin/components/forms/field-types/RichText/elements/getCommonBlock.tsx b/src/admin/components/forms/field-types/RichText/elements/getCommonBlock.tsx new file mode 100644 index 0000000000..f88ca817b2 --- /dev/null +++ b/src/admin/components/forms/field-types/RichText/elements/getCommonBlock.tsx @@ -0,0 +1,19 @@ +import { Editor, Node, NodeEntry } from 'slate'; + +export const getCommonBlock = (editor: Editor): NodeEntry => { + const range = Editor.unhangRange(editor, editor.selection, { voids: true }); + + const [common, path] = Node.common( + editor, + range.anchor.path, + range.focus.path, + ); + + if (Editor.isBlock(editor, common) || Editor.isEditor(common)) { + return [common, path]; + } + return Editor.above(editor, { + at: path, + match: (n) => Editor.isBlock(editor, n) || Editor.isEditor(n), + }); +}; diff --git a/src/admin/components/forms/field-types/RichText/elements/indent/index.tsx b/src/admin/components/forms/field-types/RichText/elements/indent/index.tsx index a9ab2b3df8..17ba34aefd 100644 --- a/src/admin/components/forms/field-types/RichText/elements/indent/index.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/indent/index.tsx @@ -1,11 +1,10 @@ import React, { useCallback } from 'react'; import { useSlate, ReactEditor } from 'slate-react'; -import { Editor, Element, Transforms } from 'slate'; +import { Editor, Element, Text, Transforms } from 'slate'; import IndentLeft from '../../../../../icons/IndentLeft'; import IndentRight from '../../../../../icons/IndentRight'; import { baseClass } from '../Button'; import isElementActive from '../isActive'; -import listTypes from '../listTypes'; const indentType = 'indent'; @@ -25,26 +24,30 @@ const indent = { e.preventDefault(); if (dir === 'left') { - Transforms.unwrapNodes(editor, { - match: (n) => Element.isElement(n) && [indentType, ...listTypes].includes(n.type), - split: true, - mode: 'lowest', - }); + // Transforms.unwrapNodes(editor, { + // match: (n) => Element.isElement(n) && [indentType, ...listTypes].includes(n.type), + // split: true, + // mode: 'lowest', + // }); - if (isElementActive(editor, 'li')) { - const [, parentLocation] = Editor.parent(editor, editor.selection); - const [, parentDepth] = parentLocation; + // if (isElementActive(editor, 'li')) { + // const [, parentLocation] = Editor.parent(editor, editor.selection); + // const [, parentDepth] = parentLocation; - if (parentDepth > 0 || parentDepth === 0) { - Transforms.unwrapNodes(editor, { - match: (n) => Element.isElement(n) && n.type === 'li', - split: true, - mode: 'lowest', - }); - } else { - Transforms.unsetNodes(editor, ['type']); - } - } + // if (parentDepth > 0 || parentDepth === 0) { + // Transforms.unwrapNodes(editor, { + // match: (n) => Element.isElement(n) && n.type === 'li', + // split: true, + // mode: 'lowest', + // }); + // } else { + // Transforms.unsetNodes(editor, ['type']); + // } + // } + + const [previousNode, previousNodePath] = Editor.parent(editor, editor.selection, { depth: 4 }); + + console.log({ previousNode, previousNodePath }); } if (dir === 'right') { @@ -52,55 +55,59 @@ const indent = { const isCurrentlyUL = isElementActive(editor, 'ul'); if (isCurrentlyOL || isCurrentlyUL) { - let hasText = false; + // Get the path of the first selected li - + // Multiple lis could be selected + // and the selection may start in the middle of the first li + const [[, firstSelectedLIPath]] = Array.from(Editor.nodes(editor, { + mode: 'lowest', + match: (node) => Element.isElement(node) && node.type === 'li', + })); - if (editor.selection) { - const leafNode = Editor.leaf(editor, editor.selection.focus); - if (leafNode) { - const [leaf] = leafNode; - hasText = leaf.text.length > 0; + // Is the first selected li the first in its list? + const hasPrecedingLI = firstSelectedLIPath[firstSelectedLIPath.length - 1] > 0; + + // If the first selected li is NOT the first in its list, + // we need to inject it into the prior li + if (hasPrecedingLI) { + const [, precedingLIPath] = Editor.previous(editor, { + at: firstSelectedLIPath, + }); + + const [precedingLIChildren] = Editor.node(editor, [...precedingLIPath, 0]); + const precedingLIChildrenIsText = Text.isText(precedingLIChildren); + + if (precedingLIChildrenIsText) { + // Wrap the prior li text content so that it can be nested next to a list + Transforms.wrapNodes(editor, { children: [] }, { at: [...precedingLIPath, 0] }); } - } - if (hasText) { - Transforms.wrapNodes(editor, { - type: 'li', - children: [], - }); - Transforms.wrapNodes(editor, { type: isCurrentlyOL ? 'ol' : 'ul', children: [{ text: ' ' }] }); - Transforms.setNodes(editor, { type: 'li' }); - } else { - const [previousNode, previousNodePath] = Editor.previous(editor, { - at: editor.selection.focus, + // Move the selected lis after the prior li contents + Transforms.moveNodes(editor, { + to: [...precedingLIPath, 1], + match: (node) => Element.isElement(node) && node.type === 'li', + mode: 'lowest', }); - Transforms.removeNodes(editor); - - Transforms.insertNodes( + // Wrap the selected lis in a new list + Transforms.wrapNodes( editor, - [ - { - children: [ - previousNode, - ], - }, - { - type: isCurrentlyOL ? 'ol' : 'ul', - children: [ - { - type: 'li', - children: [ - { - text: '', - }, - ], - }, - ], - }, - ], { - at: previousNodePath, - select: true, + type: isCurrentlyOL ? 'ol' : 'ul', children: [], + }, + { + match: (node) => Element.isElement(node) && node.type === 'li', + mode: 'lowest', + }, + ); + } else { + Transforms.wrapNodes( + editor, + { + type: isCurrentlyOL ? 'ol' : 'ul', children: [{ type: 'li', children: [] }], + }, + { + match: (node) => Element.isElement(node) && node.type === 'li', + mode: 'lowest', }, ); } diff --git a/src/admin/components/forms/field-types/RichText/elements/isActive.tsx b/src/admin/components/forms/field-types/RichText/elements/isActive.tsx index d75c20c0f0..a99edb30cb 100644 --- a/src/admin/components/forms/field-types/RichText/elements/isActive.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/isActive.tsx @@ -1,6 +1,6 @@ import { Editor, Element } from 'slate'; -const isElementActive = (editor, format) => { +const isElementActive = (editor: Editor, format: string): boolean => { if (!editor.selection) return false; const [match] = Array.from(Editor.nodes(editor, { diff --git a/src/admin/components/forms/field-types/RichText/elements/isListActive.ts b/src/admin/components/forms/field-types/RichText/elements/isListActive.ts index 2c6126da2a..d1052cac1b 100644 --- a/src/admin/components/forms/field-types/RichText/elements/isListActive.ts +++ b/src/admin/components/forms/field-types/RichText/elements/isListActive.ts @@ -1,23 +1,22 @@ -import { Ancestor, Editor, Element, NodeEntry } from 'slate'; +import { Editor, Element } from 'slate'; +import { getCommonBlock } from './getCommonBlock'; const isListActive = (editor: Editor, format: string): boolean => { - let parentLI: NodeEntry; + if (!editor.selection) return false; + const [, topmostSelectedNodePath] = getCommonBlock(editor); - try { - parentLI = Editor.parent(editor, editor.selection); - } catch (e) { - // swallow error, Slate - } + const [match] = Array.from(Editor.nodes(editor, { + at: topmostSelectedNodePath, + mode: 'lowest', + match: (node, path) => { + return !Editor.isEditor(node) + && Element.isElement(node) + && node.type === format + && path.length >= topmostSelectedNodePath.length - 1; + }, + })); - if (parentLI?.[1]?.length > 0) { - const ancestor = Editor.above(editor, { - at: parentLI[1], - }); - - return Element.isElement(ancestor[0]) && ancestor[0].type === format; - } - - return false; + return !!match; }; export default isListActive; diff --git a/src/admin/components/forms/field-types/RichText/elements/isWithinListItem.ts b/src/admin/components/forms/field-types/RichText/elements/isWithinListItem.ts new file mode 100644 index 0000000000..13ed8064e6 --- /dev/null +++ b/src/admin/components/forms/field-types/RichText/elements/isWithinListItem.ts @@ -0,0 +1,17 @@ +import { Editor, Element, Ancestor, NodeEntry } from 'slate'; + +export const isWithinListItem = (editor: Editor): boolean => { + let parentLI: NodeEntry; + + try { + parentLI = Editor.parent(editor, editor.selection); + } catch (e) { + // swallow error, Slate + } + + if (Element.isElement(parentLI?.[0]) && parentLI?.[0]?.type === 'li') { + return true; + } + + return false; +}; diff --git a/src/admin/components/forms/field-types/RichText/elements/toggle.tsx b/src/admin/components/forms/field-types/RichText/elements/toggle.tsx index 6e94a10c96..7511986ed2 100644 --- a/src/admin/components/forms/field-types/RichText/elements/toggle.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/toggle.tsx @@ -1,15 +1,23 @@ -import { Transforms } from 'slate'; +import { Editor, Transforms } from 'slate'; import { ReactEditor } from 'slate-react'; import isElementActive from './isActive'; +import { isWithinListItem } from './isWithinListItem'; -const toggleElement = (editor, format) => { +const toggleElement = (editor: Editor, format: string): void => { const isActive = isElementActive(editor, format); let type = format; + const isWithinLI = isWithinListItem(editor); + if (isActive) { type = undefined; } + if (!isActive && isWithinLI) { + const block = { type: 'li', children: [] }; + Transforms.wrapNodes(editor, block); + } + Transforms.setNodes(editor, { type }); ReactEditor.focus(editor); diff --git a/src/admin/components/forms/field-types/RichText/elements/toggleList.tsx b/src/admin/components/forms/field-types/RichText/elements/toggleList.tsx index d28970973d..6c447a8a93 100644 --- a/src/admin/components/forms/field-types/RichText/elements/toggleList.tsx +++ b/src/admin/components/forms/field-types/RichText/elements/toggleList.tsx @@ -1,29 +1,40 @@ -import { Element, Transforms } from 'slate'; +import { Editor, Element, Transforms } from 'slate'; import { ReactEditor } from 'slate-react'; -import isElementActive from './isActive'; +import isListActive from './isListActive'; import listTypes from './listTypes'; -const toggleList = (editor, format) => { - const isActive = isElementActive(editor, format); - const isList = listTypes.includes(format); +const toggleList = (editor: Editor, format: string): void => { + let currentListFormat: string; - let type = format; + if (isListActive(editor, 'ol')) currentListFormat = 'ol'; + if (isListActive(editor, 'ul')) currentListFormat = 'ul'; - if (isActive) { - type = undefined; - } else if (isList) { - type = 'li'; - } + // If the format is currently active, + // unwrap the list and set li type to undefined + if (currentListFormat === format) { + Transforms.unwrapNodes(editor, { + match: (n) => Element.isElement(n) && listTypes.includes(n.type), + mode: 'lowest', + }); - Transforms.unwrapNodes(editor, { - match: (n) => Element.isElement(n) && listTypes.includes(n.type as string), - split: true, - mode: 'lowest', - }); + Transforms.setNodes(editor, { type: undefined }); - Transforms.setNodes(editor, { type }); - - if (!isActive && isList) { + // Otherwise, if a list is active and we are changing it, + // change it + } else if (currentListFormat !== format) { + Transforms.setNodes( + editor, + { + type: format, + }, + { + match: (node) => Element.isElement(node) && listTypes.includes(node.type), + mode: 'lowest', + }, + ); + // Otherwise we can assume that we should just activate the list + } else { + Transforms.setNodes(editor, { type: 'li' }); const block = { type: format, children: [] }; Transforms.wrapNodes(editor, block); } diff --git a/test/fields/collections/RichText/index.ts b/test/fields/collections/RichText/index.ts index e9f42d6774..d102202627 100644 --- a/test/fields/collections/RichText/index.ts +++ b/test/fields/collections/RichText/index.ts @@ -271,6 +271,31 @@ export const richTextBulletsDoc = { { type: 'ul', children: [ + { + type: 'li', + children: [ + { + children: [ + { + text: 'I am semantically connected to my sub-bullets', + }, + ], + }, + { + type: 'ul', + children: [ + { + type: 'li', + children: [ + { + text: 'I am sub-bullets that are semantically connected to the parent bullet', + }, + ], + }, + ], + }, + ], + }, { children: [ {