Implementing line-breaking logic for Block formatted nodes
mbomfim33 opened this issue · comments
Problem
Not quite a problem, I'm just keen in contributing to the repo by explaining how I implemented a line breaking logic, which seems to be a common feature for rich text editors, and how this could be included in Slate's documentation or examples. Loving Slate so far by the way!
Solution
This was written for a React application, I'm not sure about other frameworks, but feel it should be the same structure.
First, I've implemented a handleKeyDown
method to check what is thetype
of the current Node
:
import { Editor, Range } from 'slate'
import { handleEnterOnHeader, handleEnterOnListItem } from './enterHandlers'
export const handleKeyDown = (event, editor) => {
if (event.key === 'Enter') {
const { selection } = editor
if (!selection || Range.isExpanded(selection)) {
return
}
const [node, path] = Editor.node(editor, selection, { depth: 1 })
switch (node.type) {
case 'heading-one':
case 'heading-two':
case 'heading-three':
return handleEnterOnHeader(editor, event, node, path)
case 'numbered-list':
case 'bulleted-list':
return handleEnterOnListItem(editor, event, node, path)
default:
break
}
}
}
This method will then call the appropriate enterHandler
(creative name, yeah). The idea was to simply validate a couple of conditions based on how Slack's editor works right now:
import { Transforms, Editor, Point, Range, Path } from 'slate'
import { CustomCommands } from '../commands/CustomCommands'
export const handleEnterOnHeader = (editor, event, node, path) => {
const { selection } = editor
if (!selection || !Range.isCollapsed(selection)) {
return false
}
// Ensure the anchor point is valid
if (!Point.isPoint(selection.anchor)) {
return false
}
const start = Editor.start(editor, path)
const end = Editor.end(editor, path)
// Check if start and end are valid points
if (!Point.isPoint(start) || !Point.isPoint(end)) {
return false
}
if (Point.equals(selection.anchor, end)) {
// Cursor is at the end of the header block
event.preventDefault()
const isEmpty = node.children[0].text === ''
if (isEmpty) {
// Convert to paragraph if the node is a header and empty
Transforms.setNodes(editor, { type: 'paragraph' })
} else {
// Insert a new paragraph
const paragraphNode = { type: 'paragraph', children: [{ text: '' }] }
Transforms.insertNodes(editor, paragraphNode)
}
return true
}
// If cursor is not at the end, allow default behavior
return false
}
export const handleEnterOnListItem = (editor, event, node) => {
const { selection } = editor
if (!selection) return false
// Get the current list item based on the selection
const [listItem, listItemPath] = Editor.node(editor, selection.focus.path, { depth: 2 })
if (listItem.type !== 'list-item') {
return false
}
const isEmpty = listItem.children[0].text === ''
const isLastItemInList = listItemPath[listItemPath.length - 1] === node.children.length - 1
if (isEmpty && isLastItemInList) {
// If it's the last item and empty, exit the list
event.preventDefault()
CustomCommands.toggleBlock(editor, node.type)
// Move cursor to the newly created node
// Find the path for the next node (which should now be a paragraph)
const nextPath = Path.next(listItemPath.slice(0, -1))
// Move the selection to the start of the next node
Transforms.select(editor, Editor.start(editor, nextPath))
return true
} else {
return false
}
}
there's a lot of room for improvement on the code, but bear with me
In this scenario CustomCommands.toggleBlock
is the same as the toggleBlock()
on the Rich Text example.
I'm pretty sure I'm not even following all the best practices here, I still have to review the documentation further to understand how many of those manually typed conditions I can actually replace with Slate's own interfaces, but you can get the picture. In the end, I'm just using this handler on the Editable
component:
const onKeyDown = useCallback((event) => handleKeyDown(event, editor), [editor])
// ... rest of the file
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
// ...
<Editable
// ...
onKeyDown={onKeyDown}
/>
</Slate>
Alternatives
Couldn't find any alternatives to this, so I just did it myself, let me know if I'm being redundant.
Context
Apologies if this is already in the roadmap, or if this was already answered somewhere else, although I believe this would be a really nice addition to either the examples or the documentation.
iirc Plate ( https://github.com/udecode/plate ) handles some but probably not all of these scenarios. It might be worth looking there and contributing any gaps.