diff --git a/src/TextBlock/TextBlockEdit.jsx b/src/TextBlock/TextBlockEdit.jsx index 5ebe9351..26ef1769 100644 --- a/src/TextBlock/TextBlockEdit.jsx +++ b/src/TextBlock/TextBlockEdit.jsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; import { Editor, Transforms, Range, Node, Point } from 'slate'; import SlateEditor from './../editor'; import { - fixSelection, + // fixSelection, isCursorAtBlockEnd, isCursorAtBlockStart, } from './../editor/utils'; @@ -18,6 +18,121 @@ import ShortcutListing from './ShortcutListing'; import { LISTTYPES } from './constants'; import { withHandleBreak } from './decorators'; +function getPreviousBlock(index, properties) { + if (index === 0) return; + + // join this block with previous block, if previous block is slate + const blocksFieldname = getBlocksFieldname(properties); + const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); + + const blocks_layout = properties[blocksLayoutFieldname]; + const prevBlockId = blocks_layout.items[index - 1]; + const prevBlock = properties[blocksFieldname][prevBlockId]; + return prevBlock; +} + +function isCursorInList(editor) { + const [listItemWithSelection, listItemWithSelectionPath] = Editor.above( + editor, + { + match: (n) => n.type === 'list-item', + }, + ); + + // whether the selection is inside a list item + const listItemCase = + Range.isCollapsed(editor.selection) && listItemWithSelection; + + return [listItemCase, listItemWithSelectionPath, listItemCase]; +} + +function handleBackspaceInList(editor, prevBlock) { + const [listItemWithSelection, listItemWithSelectionPath] = Editor.above( + editor, + { + match: (n) => n.type === 'list-item', + }, + ); + + // whether the selection is inside a list item + const listItemCase = + Range.isCollapsed(editor.selection) && listItemWithSelection; + + // if the selection is collapsed and at node and offset 0 + // or collapsed inside a list item + if (isCursorAtBlockStart(editor) || listItemCase) { + // are we in a list-item and is cursor at the beginning of the list item? + if (listItemCase && editor.selection.anchor.offset === 0) { + if ( + Node.parent(editor, listItemWithSelectionPath).children.indexOf( + listItemWithSelection, + ) === 0 + ) { + // the cursor is inside the first list-item + event.stopPropagation(); + event.preventDefault(); + return false; // TODO: join with previous
  • element, if exists + } + // else handle by deleting the list-item + Transforms.liftNodes(editor); + Transforms.mergeNodes(editor); + Transforms.mergeNodes(editor, { at: [1] }); + //console.log('editor.children', editor.children); + + event.stopPropagation(); + event.preventDefault(); + + return; + } + } + return true; +} + +function handleBackspaceInText(editor, prevBlock) { + // To work around current architecture limitations, read the value + // from previous block. Replace it in the current editor (over + // which we have control), join with current block value, then use + // this result for previous block, delete current block + + const prev = prevBlock.value; + + // collapse the selection to its start point + Transforms.collapse(editor, { edge: 'start' }); + + // TODO: do we really want to insert this text here? + + // insert a space before the left edge of the selection + editor.apply({ + type: 'insert_text', + path: [0, 0], + offset: 0, + text: ' ', + }); + + // collapse the selection to its start point + Transforms.collapse(editor, { edge: 'start' }); + + // insert the contents of the previous editor into the current editor + Transforms.insertNodes(editor, prev, { + at: Editor.start(editor, []), + }); + + // not needed currently: delete the useless space inserted above + //Editor.deleteBackward(editor, { unit: 'character' }); + + // merge the contents separated by the collapsed selection + Transforms.mergeNodes(editor); +} + +function blockIsEmpty(editor) { + const value = editor.children; + // TODO: this is very optimistic, we might have void nodes that are + // meaningful. We should test if only one child, with empty text + if (plaintext_serialize(value || []).length === 0) { + return true; + } +} + const TextBlockEdit = (props) => { const { data, @@ -104,124 +219,52 @@ const TextBlockEdit = (props) => { }, Backspace: ({ editor, event }) => { - const { value } = data; - - // can be undefined - const [listItemWithSelection, listItemWithSelectionPath] = Editor.above( - editor, - { - match: (n) => n.type === 'list-item', - }, - ); - - // whether the selection is inside a list item - const listItemCase = - Range.isCollapsed(editor.selection) && listItemWithSelection; - - // if the selection is collapsed and at node and offset 0 - // or collapsed inside a list item - if (isCursorAtBlockStart(editor) || listItemCase) { - // TODO: this is very optimistic, we might have void nodes that are - // meaningful. We should test if only one child, with empty text - if (plaintext_serialize(value || []).length === 0) { - event.preventDefault(); - return onDeleteBlock(block, true); - } - - // are we in a list-item and is cursor at the beginning of the list item? - if (listItemCase && editor.selection.anchor.offset === 0) { - if ( - Node.parent(editor, listItemWithSelectionPath).children.indexOf( - listItemWithSelection, - ) === 0 - ) { - // the cursor is inside the first list-item - event.stopPropagation(); - event.preventDefault(); - return false; // TODO: join with previous
  • element, if exists - } - // else handle by deleting the list-item - Transforms.liftNodes(editor); - Transforms.mergeNodes(editor); - Transforms.mergeNodes(editor, { at: [1] }); - //console.log('editor.children', editor.children); - - event.stopPropagation(); - event.preventDefault(); - - return; - } - - // join this block with previous block, if previous block is slate - const blocksFieldname = getBlocksFieldname(properties); - const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); - - const blocks_layout = properties[blocksLayoutFieldname]; - const prevBlockId = blocks_layout.items[index - 1]; - const prevBlock = properties[blocksFieldname][prevBlockId]; - - if (prevBlock['@type'] !== 'slate') { - return; - } - - // To work around current architecture limitations, read the value - // from previous block. Replace it in the current editor (over - // which we have control), join with current block value, then use - // this result for previous block, delete current block - - event.stopPropagation(); + if (blockIsEmpty(editor)) { event.preventDefault(); + return onDeleteBlock(block, true); + } - const prev = prevBlock.value; + const [prevBlock = {}, prevBlockId] = getPreviousBlock( + index, + properties, + ); - // collapse the selection to its start point - Transforms.collapse(editor, { edge: 'start' }); + if (prevBlock['@type'] !== 'slate') return; - // TODO: do we really want to insert this text here? + const isAtBlockStart = isCursorAtBlockStart(editor); - // insert a space before the left edge of the selection - editor.apply({ - type: 'insert_text', - path: [0, 0], - offset: 0, - text: ' ', - }); + if (!isAtBlockStart) return; - // collapse the selection to its start point - Transforms.collapse(editor, { edge: 'start' }); + event.stopPropagation(); + event.preventDefault(); - // insert the contents of the previous editor into the current editor - Transforms.insertNodes(editor, prev, { - at: Editor.start(editor, []), - }); + if (isCursorInList(editor)) { + handleBackspaceInList(editor); + } else { + handleBackspaceInText(editor); + } - // not needed currently: delete the useless space inserted above - //Editor.deleteBackward(editor, { unit: 'character' }); - - // merge the contents separated by the collapsed selection - Transforms.mergeNodes(editor); - - const selection = JSON.parse(JSON.stringify(editor.selection)); - const combined = JSON.parse(JSON.stringify(editor.children)); - - // TODO: don't remove undo history, etc - // TODO: after Enter, the current filled-with-previous-block - // block is visible for a fraction of second - - // setTimeout is needed to ensure setState has been successfully - // executed in Form.jsx. See - // https://github.com/plone/volto/issues/1519 - onDeleteBlock(block, true); - setTimeout(() => { - onChangeBlock(prevBlockId, { - '@type': 'slate', - value: combined, - selection, - plaintext: plaintext_serialize(combined || []), - }); + const selection = JSON.parse(JSON.stringify(editor.selection)); + const combined = JSON.parse(JSON.stringify(editor.children)); + + // TODO: don't remove undo history, etc + // TODO: after Enter, the current filled-with-previous-block + // block is visible for a fraction of second + + // setTimeout ensures setState has been successfully + // executed in Form.jsx. See + // https://github.com/plone/volto/issues/1519 + setTimeout(() => { + onChangeBlock(prevBlockId, { + '@type': 'slate', + value: combined, + selection, + plaintext: plaintext_serialize(combined || []), }); - } - return true; + setTimeout(() => onDeleteBlock(block, true)); + }); + + return; }, ...settings.slate?.keyDownHandlers, @@ -229,12 +272,11 @@ const TextBlockEdit = (props) => { }, [ block, blockNode, - data, index, - onChangeBlock, - onDeleteBlock, onFocusNextBlock, onFocusPreviousBlock, + onDeleteBlock, + onChangeBlock, properties, ]);