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
- Enable
toc_jsand add its TOC via the provided block. - Create a node with multiple headings (
h2/h3). - Attempt to place the TOC inside the body (e.g., after the first paragraph) without Layout Builder or theme overrides.
- 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_jsblock (heading levels/depth, label, container classes, etc.) on a per-Text-Format basis. - Programmatically renders the existing
toc_jsblock at the token location, so output and behavior match the block 1:1. - Attaches the
toc_jslibrary 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_filtersubmodule) appears under Configuration → Content authoring → Text formats and editors. - Filter settings fieldset per Text Format mirrors the
toc_jsblock 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_jsblock plugin; no public API changes intoc_jsitself.
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
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:
Comments
Comment #3
gabriel.passarelli commentedComment #4
gabriel.passarelli commentedWhile implementing
toc_js_filter, I noticed we now define the same configuration fields and default values in three places:toc_js.moduleTocJsBlock.phpTocJsFilter.phpI’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.
Comment #6
mably commentedHi @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.
Comment #7
gabriel.passarelli commentedHi @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.modulefile.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!
Comment #8
mably commented@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?
Comment #9
gabriel.passarelli commented@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
Comment #10
mably commentedLooks like the table of contents are not rendering anymore 😉
Comment #11
mably commentedComment #12
mably commentedOk, node field ToC is displaying again 😅
But Node block configuration is lost.
We are probably missing some default values somewhere.
Comment #13
mably commentedLooks like everything is working again. 🎉😅
@gabriel.passarelli can you have a final look at this issue's MR please?
Comment #14
hfernandes commentedI added some comments to the MR.
Comment #15
mably commentedThanks @hfernandes for the review!
I updated the code based on your suggestions.
Feel free to RTBC if everything seems ok.
Comment #16
mably commentedComment #17
mably commentedAdded a few functional tests.
Can we have an RTBC please?
Comment #18
gabriel.passarelli commented@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:
Comment #19
mably commentedThanks for the review @gabriel.passarelli !
Let's merge this.
Comment #21
mably commentedComment #23
mably commentedReleased in 3.4.0-rc1.
Comment #25
flocondetoileA bit late, but for reference, such behavior could be performed too with the block token module.
Comment #26
mably commentedActually we ended not using blocks at all. But still interesting to know.