Problem/Motivation

toc_js currently exposes its TOC via a block. This works well for Layout Builder pages or theme-level placements (e.g., in a node Twig), but it does not give editors per-content, in-body control of where the TOC appears. They cannot place the TOC after an intro paragraph, between sections, etc.

This change adds a Text Filter that lets authors insert a simple token ([toc]) at the desired location in the body. The filter reuses the existing toc_js block (and its configuration/options) and renders that block output exactly where the token appears—delivering the same frontend behavior with much finer editorial control.

Steps to reproduce

  1. Enable toc_js and add its TOC via the provided block.
  2. Create a node with multiple headings (h2/h3).
  3. Attempt to place the TOC inside the body (e.g., after the first paragraph) without Layout Builder or theme overrides.
  4. Observe: it’s not possible—only block placement is available; token-based placement is missing.

Proposed resolution

Add a CKEditor 5-compatible Text Filter that:

  • Exposes the same configuration options as the existing toc_js block (heading levels/depth, label, container classes, etc.) on a per-Text-Format basis.
  • Programmatically renders the existing toc_js block at the token location, so output and behavior match the block 1:1.
  • Attaches the toc_js library exactly once; guards against duplicate attachments.
  • Ensures correct cacheability (contexts/tags/max-age) propagate from the block to the filter result.

User interface changes

  • New filter “Toc.js shortcode: [toc]” (provided by the toc_js_filter submodule) appears under Configuration → Content authoring → Text formats and editors.
  • Filter settings fieldset per Text Format mirrors the toc_js block options (heading levels, depth, label, classes, list type, numbering, anchor prefix, scope selector).
  • No new standalone admin pages.

API changes

  • None. Adds a new Text Filter in a submodule that reuses the existing toc_js block plugin; no public API changes in toc_js itself.

Data model changes

  • No new entities/tables.
  • New config schema for the filter’s per-Text-Format settings stored with Text Format config; keys mirror the block’s options.

Issue fork toc_js-3556279

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

  • 3556279-add-a-text Comparechanges, plain diff MR !46
  • 3.x Comparecompare

Comments

gabriel.passarelli created an issue. See original summary.

gabriel.passarelli’s picture

Assigned: gabriel.passarelli » Unassigned
gabriel.passarelli’s picture

Status: Active » Needs review

While implementing toc_js_filter, I noticed we now define the same configuration fields and default values in three places:

  • toc_js.module
  • TocJsBlock.php
  • TocJsFilter.php

I’d suggest a follow-up to refactor these shared options into a single, reusable place that both the block and filter can consume. I didn’t include that larger refactor in this MR to keep the scope focused, but I’m happy to work on it as a separate change.

mably made their first commit to this issue’s fork.

mably’s picture

Hi @gabriel.passarelli I worked on the code refactoring you mention in your last message.

Could you have a look at the MR and check if everything seems ok to you?

The refactoring allowed to get rid of the BlockManager usage in the filter.

gabriel.passarelli’s picture

Hi @mably,

Thank you for your quick response, and sorry for my late reply, I was away for the past couple of days.

I really appreciate your help with the refactor. I like the trait-based approach; there’s just one point I wanted to mention. In toc_js.module we’re also defining the form settings again within a hook_form_alter. I’m not sure if we'd be able to use a trait inside a .module file.

We could either update this hook to use a different approach, or instead of a trait, convert it into a service and inject it as a dependency into the other classes and in the .module file.

Once again, thanks for considering this feature and for taking the time to work on the refactor!

mably’s picture

@gabriel.passarelli have implemented a service to generate the various forms.

Could you please review it and let me know if everything looks good to you?

gabriel.passarelli’s picture

@mably, I think it was a little bit confused with the module using the trait and still using the trait methods where the service was injected.
I did a little bit more of the refactor and removed the trait and consolidate everything with the service and focus on using the service methods on all places. Please have a look and let me know what you think

mably’s picture

Looks like the table of contents are not rendering anymore 😉

mably’s picture

Version: 3.3.0 » 3.x-dev
Status: Needs review » Needs work
mably’s picture

Ok, node field ToC is displaying again 😅

But Node block configuration is lost.

We are probably missing some default values somewhere.

mably’s picture

Status: Needs work » Needs review

Looks like everything is working again. 🎉😅

@gabriel.passarelli can you have a final look at this issue's MR please?

hfernandes’s picture

Status: Needs review » Needs work

I added some comments to the MR.

mably’s picture

Thanks @hfernandes for the review!

I updated the code based on your suggestions.

Feel free to RTBC if everything seems ok.

mably’s picture

Status: Needs work » Needs review
mably’s picture

Added a few functional tests.

Can we have an RTBC please?

gabriel.passarelli’s picture

Status: Needs review » Reviewed & tested by the community

@mably thanks for all the effort you've put into this.
I've tested with my content the following variations and all worked as expected:

  • Default Toc.Js Block placed using block layout
  • Default Toc.Js Block placed using layout builder
  • Toc.Js Block Per Node placed using layout builder without overwriting settings
  • Toc.Js Block Per Node placed using layout builder overwriting settings
  • Toc.Js filter
mably’s picture

Thanks for the review @gabriel.passarelli !

Let's merge this.

mably’s picture

Status: Reviewed & tested by the community » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably’s picture

Released in 3.4.0-rc1.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

flocondetoile’s picture

A bit late, but for reference, such behavior could be performed too with the block token module.

mably’s picture

Actually we ended not using blocks at all. But still interesting to know.