Do you know what the top requested feature of blogdown has been in the past six years? Code folding. I have rarely seen 34 upvotes on a GitHub issue in projects that I maintain.
The original request was made in 2017 (thanks, Jasper Slingsby). Two years
later, when I was thinking about collapsing code blocks for xaringan slides,
I experimented with the <details>
tag (thanks, Emi Tanaka, a cool
hacker as usual). I had a intuition that the
implementation of code folding could be extremely simple.
The natural question to ask is, why not just reuse this feature from the rmarkdown package? Well, in terms of JS and CSS dependencies, rmarkdown is too heavy and complicated in my eyes—there is a JS file, which is tied to Bootstrap and jQuery (two more things that I do not wish to rely on), and the CSS code is scattered through a complicated Pandoc template.
On May 31st this year, Xiangyun said that the only missing feature of blogdown / Hugo sites he would want was code folding. Unfortunately I went on vacation soon, so I did not look into it further. To close this browser tab (among dozens of others), I spent a little more time on this task today.
Six lines of JS to fold all code blocks
The idea is quite simple: create a <details>
element containing a <summary>
,
and move a code block (<pre>
) into the <details>
element.
document.querySelectorAll('pre').forEach(pre => {
const d = document.createElement('details');
d.innerHTML = '<summary>Details</summary>';
pre.before(d); // insert <details> before <pre>
d.append(pre); // move <pre> into <details>
});
That’s it. You can open any web page that contains <pre>
blocks, paste the
code into the JavaScript console of your browser (in Developer Tools), and see
code blocks being folded into Details
elements, on which you can click to
unfold.1
Fold source but not output blocks
If you want to fold only source code blocks but not output, it is not hard, either. For HTML documents generated by knitr / R Markdown / Quarto, source code blocks often have classes, and output blocks do not. We can tweak our selector above based on this fact.
document.querySelectorAll('pre[class], pre > code[class]').forEach(el => {
const d = document.createElement('details');
d.innerHTML = '<summary>Details</summary>';
const pre = el.tagName === 'CODE' ? el.parentNode : el;
pre.before(d); // insert <details> before <pre>
d.append(pre); // move <pre> into <details>
});
With pre[class], pre > code[class]
, we are selecting two types of elements:
<pre>
with classes, and <pre>
’s direct child <code>
with classes. This is
because different Markdown renderers can put the class on either <pre>
or
<code>
.
If you run the above code on a page, <pre class="r">
and
<pre><code class="r">
will be folded (the class name can be any other names),
but <pre><code>
will not.
Fold or unfold all blocks
You may also want a button to fold or unfold all blocks. Again, that is simple to implement. For example, you can first provide a button on your HTML page:
<button id="toggle-all">Toggle Code</button>
Then add a click
event to it via JavaScript:
document.getElementById('toggle-all').onclick = (e) => {
[...document.getElementsByTagName('details')].forEach(el => {
el.toggleAttribute('open');
});
};
You do not have to use the click
event of a button, but can use any event of
any element. The key here is to toggle the open
attribute of <details>
. If
it has the open
attribute, it is unfolded, otherwise it is folded.
Aside: a button from a decade ago
In fact, I have already implemented such a toggle button ten years ago, and the
JS code is in the knitr
package (I
just rewrote it with modern JS today). At that time, I was not aware of the
<details>
tag in HTML, and used the CSS attribute display
(block
or
none
) to control the visibility of code blocks.
Fold anything: a general solution
Once you have learned CSS
selectors (which are very
flexible), you can apply the above idea to fold anything on a web page. All you
need to do is provide an appropriate selector to document.querySelectorAll()
.
For example, if you want to fold tables, just use table
as the selector; or if
you want to fold the comments section <section class="comments">
, use
section.comments
.
I have written the script
fold-details.js
and you can just load it on your page:
<script src="https://cdn.jsdelivr.net/npm/@xiee/utils/js/fold-details.min.js" defer></script>
By default, it folds code blocks that have classes, which means if you use
knitr / R Markdown / Quarto, only source code blocks will be folded. You can
customize its behavior via the data-
attributes of the <script>
tag that
loads fold-details.js
:
-
data-selector
: A selector to select elements to fold, e.g.,"pre>code[class], table"
. -
data-open
: The initial status of the details elements, e.g.,"true"
to make them open initially (by default, they are closed). -
data-label
: The label of the details elements, e.g.,"View Details"
. -
data-tag-name
: In the labels, whether to display the tag names of elements that are folded, e.g.,"true"
to append<CODE>
to the labels of details elements folding code blocks. -
data-button
:-
If
true
, a button (<button id="toggle-all">
) to toggle all details on the page will be created ifdata-parent
is also provided as a selector to find an existing parent element for the button, e.g.,data-parent="body"
means to use the document body as the parent of the button (in this case, you may want to style the button with CSSposition: absolute;
). The button can be inserted into the parent element by different positions, which can be specified bydata-position
; see theinsertAdjacentElement()
method for possible values (the default isafterbegin
). -
Alternatively, you can specify
data-button
as a selector to select an existing element (not necessarily a button) on the page to act as the button. For example, if you have already gotten a<span id="my-toggle">
on your page, you can specifydata-button="#my-toggle"
. -
By default, no button will be created or used.
-
-
data-button-label
: The button label for closing all details, e.g.,"Hide Details"
. -
data-button-label2
: The button label for opening all details. e.g.,"Show Details"
.
An example
I have loaded fold-details.js
in this post via
<script src="path/to/fold-details.js" data-open="true" data-button="#toggle-all">
and created a button with the ID toggle-all
below:
You can click on the button to hide all details blocks in this post, which are
open initially due to the option data-open="true"
that I specified for the
script.
I have also added a selector #TableOfContents ~ p:nth-last-of-type(3)
to the
data-selector
option, so that the third paragraph from the last after the
table of contents (i.e., this paragraph) will be moved into a details tag, too.
Personally I’m happy with this general solution. It is super lightweight yet flexible, and can be applied to any element on any web page regardless of how the page was generated (blogdown/Hugo, Quarto, Jekyll, and WordPress, etc.). I hope you will find it useful. Happy folding!
-
You may not like details to “snap” on toggle, but prefer a smooth transition. That can be achieved by more JS code (which I forked and tweaked from Louis Hoebregts’s work, but honestly, I do not understand it). ↩︎