javascriptreactjsquill

How to add soft line breaks in Quill?


I'm struggling with making Quill 2.0 support soft line breaks in React 19. The library I'm using is react-quill-new

Basically I want editor to add a <br> tag without the surrounding <p> tags (which is default behavior, same as with Enter key).I found this solution Extending Quill to support soft line breaks and implemented it in my code, but the code won't run saying:

ParchmentError: [Parchment] Unable to create softbreak blot

Here's the complete component

import Embed from 'quill/blots/embed'
import { PropsWithChildren, ReactElement, useState } from 'react'
import ReactQuill, { Quill } from 'react-quill-new'
import 'react-quill-new/dist/quill.snow.css'

const Delta = Quill.import('delta')

export class SoftLineBreakBlot extends Embed {
  static blotName = 'softbreak'
  static tagName = 'br'
  static className = 'softbreak'
}
Quill.register(SoftLineBreakBlot)

export function shiftEnterHandler(this: any, range) {
  const currentLeaf = this.quill.getLeaf(range.index)[0]
  const nextLeaf = this.quill.getLeaf(range.index + 1)[0]
  this.quill.insertEmbed(range.index, 'softbreak', true, Quill.sources.USER)
  // Insert a second break if:
  // At the end of the editor, OR next leaf has a different parent (<p>)
  if (nextLeaf === null || currentLeaf.parent !== nextLeaf.parent) {
    this.quill.insertEmbed(range.index, 'softbreak', true, Quill.sources.USER)
  }
  // Now that we've inserted a line break, move the cursor forward
  this.quill.setSelection(range.index + 1, Quill.sources.SILENT)
}

export function brMatcher(node, delta) {
  let newDelta = new Delta()
  newDelta.insert({ softbreak: true })
  return newDelta
}

interface EditorProps extends PropsWithChildren {
  defaultValue?: string
  onChange?: (value: string) => void
  children?: ReactElement
}

const Editor = ({ defaultValue = '', onChange, children }: EditorProps) => {
  const [value, setValue] = useState(defaultValue)

  const modules = {
    toolbar: [
      ['bold', 'italic', 'underline', 'strike'],
      [{ list: 'ordered' }, { list: 'bullet' }],
      ['link'],
      ['clean'],
    ],
    keyboard: {
      bindings: {
        'shift enter': {
          key: 13,
          shiftKey: true,
          handler: shiftEnterHandler,
        },
      },
    },
    clipboard: {
      matchers: [['BR', brMatcher]],
    },
  }

  const formats = ['bold', 'italic', 'underline', 'strike', 'list', 'image', 'link']

  const handleChange = (newValue: string) => {
    setValue(newValue)
    if (onChange) {
      onChange(newValue)
    }
  }

  return (
    <ReactQuill
      theme="snow"
      value={value}
      onChange={handleChange}
      modules={modules}
      formats={formats}
    >
      {children}
    </ReactQuill>
  )
}
export default Editor

Solution

  • Following the previous answer, with the import of Embed like this `import Embed from 'quill/blots/embed'`, I want to provide further steps who seems to also be importants.

    1. We should overwrite the registration with the second flag `true` in `Quill.register(SoftLineBreakBlot, true)`

    2. Insert a Zero-Width space \u200B right after the "softbreak" to stabilize the cursor position after the `<br>` could also improve the unexpected behaviours before and after the line return

    3. Use "Enter" instead of key 13 seems to give better results in the keyboard bindings declaration

    4. The declaration of `softbreak` in the formats is crucial to tell Quill which embed we need to insert in the editor

    const Delta = Quill.import('delta')
    const Embed = Quill.import('blots/embed')
    
    export class SoftLineBreakBlot extends Embed {
      static blotName = 'softbreak'
      static tagName = 'br'
      static className = 'softbreak'
    }
    Quill.register(SoftLineBreakBlot, true)       // overwrite the registration
    
    function shiftEnterHandler(this: any, range) {
      this.quill.insertEmbed(range.index, 'softbreak', true, Quill.sources.USER)
      this.quill.insertText(range.index + 1, '\u200B', Quill.sources.USER)
      this.quill.setSelection(range.index + 1, Quill.sources.SILENT)
    }
    
    const brMatcher = () => new Delta().insert({ softbreak: true })
    

    Then inside the component for modules and formats declarations :

    const modules = {
      toolbar: [
        ['bold', 'italic', 'underline', 'strike'],
        [{ list: 'ordered' }, { list: 'bullet' }],
        ['link', 'clean'],
      ],
      keyboard: {
        bindings: {
          shiftEnter: {
            key: 'Enter',       // works better than 13
            shiftKey: true,
            handler: shiftEnterHandler,
          },
        },
      },
      clipboard: {
        matchers: [['BR', brMatcher]],
        matchVisual: false,
      },
    }
    
    const formats = [
      'bold', 'italic', 'underline', 'strike',
      'list',
      'image', 'link',
      'softbreak',       // quill need to know which format "embed"
    ]