PhigoroDark
Blog / Table of Contents for EditorJS

Table of Contents for EditorJS

Table of Contents for EditorJS

Intro

Editor.js is a great tool for creating rich text content. It's easy to use, and it's open source. However, it doesn't come with a built-in table of contents feature. In this post, we'll learn how to implement a table of contents for EditorJS.

Final Code

Final code for the plugin is available on GitHub

You are welcome to use it in your projects.

Usage

You can install the plugin using npm.

npm install @phigoro/editorjs-toc

To use the plugin, you need to add it to the EditorJS configuration.

const TOC = require('@phigoro/editorjs-toc');

Plugin Structure

EditorJS plugins are simple JavaScript objects that implement a few methods:

  • constructor() method is called when the plugin is created
  • render() method is called to create a UI
  • save() method extracts the Block's data from that UI
  • validate() method checks if the Block's data is valid
  • renderSettings() method is called when user clicks on the Block Actions menu

And we can also add custom methods to the plugin:

  • _getHeadings() method finds all the headings in the EditorJS data
  • _createTOC() method creates the table of contents UI

Constructor

The constructor() method is called when the plugin is created. It's a good place to initialize the plugin's properties. In our case, we'll need to store the EditorJS API and the data. We'll also create an empty array to store the headings.

constructor({data, api}){
    this.api = api;
    this.data = data;
    this.wrapper = undefined;
    this.data.items = data.items || [];
}

Render

The render() method is called to create a UI. We'll create a wrapper div element and call the _createTOC() method to create the table of contents UI.

render(){
    this.wrapper = document.createElement('div');

    if (this.data.items.length < 1) {
        this._getHeadings();
    }
    
    this._createTOC();
    return this.wrapper;
}

Getting Headings

The _getHeadings() method finds all the headings in the EditorJS data. It loops through all the blocks and checks if the block is a heading. If it is, it extracts the heading text and level. Then it creates a new object and pushes it to the items array with a random ID and reference to the block.

_getHeadings() {
    this.data.items = [];

    var blocksCount = this.api.blocks.getBlocksCount();

    for (var i = 0; i < blocksCount; i++) {
        var data = this.api.blocks.getBlockByIndex(i);

        if (data.name == 'header') {
            if (data.holder.innerText) {

                var level = data.holder.childNodes[0].childNodes[0].nodeName;
                level = parseInt(level.replace('H', ''));

                // create a new object
                var newObj = {
                    // generate random string 10 characters long
                    id: Math.random().toString(36).substring(2, 12),
                    reference: data.id,
                    text: data.holder.innerText,
                    level: level
                };

                // push the new object to the array
                this.data.items.push(newObj);
            }
        }
    }

    this._createTOC();
}

Creating the Table of Contents

The _createTOC() method creates the table of contents UI. It loops through the items array and creates a paragraph element for each heading. It then creates an anchor element and adds the paragraph element to it. Finally, it adds the anchor element to the wrapper div element.

_createTOC() {

    const headings = this.data.items;

    var divElement = document.createElement('div');

    if (headings.length < 1) {
    var message = '<p>No headings found</p>';
    divElement.innerHTML = message;
    } else {
    for (const heading of headings) {

        // create paragraph element
        var p = document.createElement('p');
        p.classList.add('toc-paragraph');

        // add class to paragraph element
        p.classList.add('toc-l-' + heading.level);

        var text = "";
        switch (heading.level) {
        case 1:
            text = '•';
            break;
        case 2:
            text = '◦';
            break;
        case 3:
            text = '▪︎';
            break
        case 4:
            text = '▫︎';
            break
        case 5:
            text = '▪︎';
            break
        case 6:
            text = '▫︎';
            break
        default:
            text = '•';
        }
        text = text + ' ' + heading.text;

        // add text to paragraph element
        p.innerHTML = text;

        // create anchor element
        var a = document.createElement('a');

        // set href attribute
        a.setAttribute('href', '#' + heading.reference);

        // add paragraph element to anchor element
        a.appendChild(p);

        // add anchor element to toc
        divElement.appendChild(a);
    }
    }

    this.wrapper.innerHTML = '';
    this.wrapper.appendChild(divElement);
}

Save

The save() method extracts the Block's data from that UI. In our case, we'll just return the items array.

save(){
    return {
        items: this.data.items
    }
}

renderSettings

The renderSettings() method is called when user clicks on the Block Actions menu. In our case, we'll just call the _getHeadings() method to update the table of contents.

renderSettings() {
    return [
        {
          icon:   `<svg>...</svg>`,
          name: 'refresh',
          label: this.api.i18n.t('Refresh'),
          toggle: 'toc',
          onActivate: () => {
                // action
                this._getHeadings();
            }
        },
    ];
}

CSS

The CSS is pretty simple. We'll just add some padding and margin to the table of contents.

a:link { text-decoration: none; }
a:hover { text-decoration: underline; }

.toc-paragraph {
    line-height: 0.6;
}

.toc-l-2 {
    padding-left: 1.0em;
}

.toc-l-3 {
    padding-left: 2.0em;
}

.toc-l-4 {
    padding-left: 3.0em;
}

.toc-l-5 {
    padding-left: 4.0em;
}

.toc-l-6 {
    padding-left: 5.0em;
}

Final Result

Here's what the final result looks like:

Table of Contents

Conclusion

That's it! We've created a simple table of contents plugin for EditorJS. You can find the full code on GitHub. EditorJS is a great tool for creating custom editors. I hope this tutorial will help you create your own plugins.

If you find any bugs or have any suggestions, please let me know in the issues section on GitHub.