How to implement a character limitation in Editor.js

4 min read

Let's suppose you want to limit the maximum length of text you allow users to enter in a form based on Editor.js.

Editor.js doesn't have such an option out of the box. Instead of this, it allows you to implement a wide kind of business logic using its API. The required behavior could be achieved using the onChange callback and some other API methods.

Idea

What we need to do:

  1. Add the onChange callback
  2. Get current editor data
  3. Calculate existed text length
  4. Trim a working block text if the limit is reached

Step 1. Add the onChange callback

const editor = new EditorJS({ // ... Other configuration properties ... /** * The onChange callback */ onChange: async (api, event) => { // method will be fired on every editor change } })

Steps 2—3. Calculate the length of Editor.js text content

Next code fragments are parts of the onChange handler.

First, save the Editor to get the current content data.

const content = await api.saver.save()

Now we need to calculate the content length. 

function couldBeCounted(block){ return 'text' in block.data // it depends on tools you use } function getBlocksTextLen(blocks){ return blocks .filter(couldBeCounted) .reduce((sum, block) => { sum += block.data.text.length return sum }, 0) } const content = await api.saver.save() const contentLen = getBlocksTextLen(content.blocks)

The couldBeCounted method is used to filter only simple Blocks containing the text property in data. And the getBlocksTextLen method just iterates through all such Blocks and sums their text length.

If you need to support different kinds of Blocks, just provide your specific logic here. For example, to support the List Block, you need to loop over the items and calculate the overall length.

Step 4. Check limit and trim

The editor can contain several Blocks so user can enter text into any of them. We will trim the current working Block. To do that, we need to calculate the other Blocks` text lengths and then we'll find the allowed limit for the current Block.

const limit = 10 if (contentLen <= limit){ return; } const workingBlock = event.detail.target const workingBlockSaved = content.blocks.filter(block => block.id === workingBlock.id).pop() const otherBlocks = content.blocks.filter(block => block.id !== workingBlock.id) const otherBlocksLen = getBlocksTextLen(otherBlocks) const workingBlockLimit = limit - otherBlocksLen

We use the event.detail to access the current Block. The "event" here is a CustomEvent passed to the onChange callback by the Editor.

Then, just trim the current Block text and update it using the Editor.js API:

api.blocks.update(workingBlock.id, { text: workingBlockSaved.data.text.substr(0, workingBlockLimit) })

And also we'll set the Caret to the end of the working Block to allow the user continuing modification of the Block.

const workingBlockIndex = event.detail.index api.caret.setToBlock(workingBlockIndex, 'end')

The final result

const editor = new EditorJS({ // ... Other configuration properties ... /** * The onChange callback */ onChange: async (api, event) => { // only count block modifications and skip events like 'block-added' etc if (event.type !== 'block-changed'){ return; } function couldBeCounted(block){ return 'text' in block.data } function getBlocksTextLen(blocks){ return blocks .filter(couldBeCounted) .reduce((sum, block) => { sum += block.data.text.length return sum }, 0) } const limit = 10 const content = await api.saver.save() const contentLen = getBlocksTextLen(content.blocks) if (contentLen <= limit){ return; } const workingBlock = event.detail.target const workingBlockIndex = event.detail.index const workingBlockSaved = content.blocks.filter(block => block.id === workingBlock.id).pop() const otherBlocks = content.blocks.filter(block => block.id !== workingBlock.id) const otherBlocksLen = getBlocksTextLen(otherBlocks) const workingBlockLimit = limit - otherBlocksLen api.blocks.update(workingBlock.id, { text: workingBlockSaved.data.text.substr(0, workingBlockLimit) }) api.caret.setToBlock(workingBlockIndex, 'end') } })

Don't forget to check the final length on the backend side as well.