ianstormtaylor / slate

A completely customizable framework for building rich text editors. (Currently in beta.)

Home Page:http://slatejs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

Thanks @dylans, I'll definitely take a look. I'll close this one out and once I get a better understanding will try contributing with it. Cheers!