Showcase
I ran across the thread TW Classic Animated Accordion Menus, and when I looked at the solutions thought, “Those are just sliders. Where are the full accordions?” When I didn’t find them (although now I find Mohammad’s KISS Macro to Create an Accordion, which is close to what I was imagining, and I’ll study it soon), I thought that, although I have no current need for one, that it might be a useful thing to try to build, to see if I can create something reasonable depending only on my current knowledge and the docs, without reaching for more help. I’m very satisfied to report that I was able to make a reasonable first pass on my own in only a few hours. The result is at Accordion Demo.
In my mind, an accordion is a fixed block with a group of headers visible as well as the content associated with one of them. When you click on a different header, the current open one slides closed as the new one slides open, keeping the overall structure the same size. In this case the blocks are tiddlers (of course!) The accordion is just a widget that accepts a filter for a collection of tiddlers, displays their titles, and when one is opened, shows its transcluded body.
It can be used with any filter for a list of tidders. Accordion 1 shows one using something like [tag[MyTag]]
. Accordion 2 shows one using Foo Bar Baz [[Bizz Buzz]]
. This one also demonstrates overriding the default height. At the moment that is the only override available.
To use it just drag these two tiddlers to your wiki:
Or test it by dragging accordion.json (3.7 KB) onto https://tiddlywiki.com and opening examples/accordion101
.
The CSS work is not original. It’s something that I modified a few years ago for a different (non-Tiddlywiki) project from one of the approximately 3.7 zillion examples available on the web. (I wish I could find the link; sorry.) I reduced and simplified the CSS, but the basic transition stuff is left intact.
The Macro
This is the current code:
\define accordion(filter, height:"300px")
<$set name="height" value="$height$" >
<$set name="acc" value=<<qualify "accordion">> >
<$set name="classname" value={{{ [<acc>addprefix[accordion ac-]] }}} >
<section class=<<classname>> >
<$list filter="$filter$ +[first[]]" counter="counter">
<$set name="id" value={{{ [<counter>addprefix[-]addprefix<acc>addprefix[ac-]] }}}>
<$wikify name="content" text={{!!text}} output="html">
<div>
<input id=<<id>> name=<<acc>> type="radio" checked="checked">
<label for=<<id>> >{{!!title}}</label>
<article><div><<content>></div></article>
</div>
</$wikify>
</$set>
</$list>
<$list filter="$filter$ +[rest[]]" counter="counter">
<$set name="id" value={{{ [<counter>add[1]addprefix[-]addprefix<acc>addprefix[ac-]] }}}>
<$wikify name="content" text={{!!text}} output="html">
<div>
<input id=<<id>> name=<<acc>> type="radio">
<label for=<<id>> >{{!!title}}</label>
<article><div><<content>></div></article>
</div>
</$wikify>
</$set>
</$list>
</section>
<style>
<$text text={{{ [<acc>addprefix[.accordion.ac-]addsuffix[ input:checked ~ article {height: ]addsuffix<height>addsuffix[ ;} ]] }}} />
</style>
</$set>
</$set>
</$set>
\end
Discussion
Duplication
I don’t think I can use the radio widget for this, as I don’t (at least for now) want to store the content choice in any tiddler. I may come back to that, but the whole mechanism is CSS-only, using the checked state of the (hidden) radio buttons in the group to determine what to show and hide, and trigger the transitions. If I’m wrong, and I can use the radio widget without specifying a tiddler/field for its state, please let me know. So I’m managing the radios manually, and that seems to work.
But there is some really stupid duplication to handle the first element of the list differently from others. I know I could usually use counter-first
to do something like this. But what I need to do is to put this on the first one:
<input id="id-xyz-1" name="accordion-xyz" type="radio" checked="checked">
and this on subsequent ones:
<input id="id-xyz-2" name="accordion-xyz" type="radio">
<input id="id-xyz-3" name="accordion-xyz" type="radio">
<!-- ... -->
and I cannot figure out a way to dynamically add the checked
attribute based on counter-first
. Of the five or so hours I spent on creating this, nearly four of them were trying to figure that out on my own. I never did it, so went with two nearly identical <$list>
s distinguished only by +[first[]]
and +[rest[]]
, a tweak to increment the counter in the latter, and the absence or presence of checked = "checked"
. This feels really stupid.
Per-invocation styling
The ability to override the height per invocation was trickier than I’d hoped, and I’m sure it can be done more simply, on two different fronts.
First, I generate something like this:
<style>
.accordion.ac-accordion-1905655034 input:checked ~ article {height: 250px ;}
</style>
The text accordion-1905655034
is stored in the variable acc
, which is generated by the qualify
macro; acc
is used several times. And 250px
is the value of the macro parameter height
. I can’t think of a better way to override this bit from the stylesheet:
.accordion input:checked ~ article {
height: 300px;
}
than by adding a stylesheet with an additional classname in the selector to increase specificity. But someone here might have a better idea.
(As I write this up, I realize that the ac-
prefixes are no longer necessary. I’ll try to remove them soon.)
Second, the way I generate that output feels way too complex. This feels ridiculous, even though it works:
<style>
<$text text={{{ [<acc>addprefix[.accordion.ac-]addsuffix[ input:checked ~ article {height: ]addsuffix<height>addsuffix[ ;} ]] }}} />
</style>
Scrollbars
I’m no expert on CSS transitions, and there’s something ugly during the transitions: appearing, resizing, and disappearing scrollbars. The original demo this came from and my own prior use of this were able to size the box appropriately to hold all the content and so we could use overflow: hidden
. We can’t do this when the content could hold arbitrarily-sized tidders, so I use overflow: auto
, and it works fine except for a minor, but annoying artifact during transitions. Perhaps someone here has more skills in this.
Questions
Beyond the obvious, “What do you think?” questions, I have several technical questions:
-
Is there a straightforward way to add an attribute dynamically in wikitext? In my case, I want to add the
checked
attribute only for the first item in my list. I struggled a long time and found nothing that would work. -
Is there a cleaner way of overriding the height of certain nested elements based on an input parameter to the macro? I end up adding a class name to the root element and writing a stylesheet, which feels heavyweight. (Note that I cannot put this on the elements themselves, because it only applies based on the CSS
:checked
pseudo-class of a sibling.) -
Whether or not the previous version is the right way to do it, is there a cleaner way to write that stylesheet than my
addprefix
/addsuffix
-laden filter? I’m simply trying to generate<style> .accordion.ac-accordion-1905655034 input:checked ~ article {height: 250px ;} </style>
using the variable
acc
, with value ofaccordion-1905655034
and the macro parameterheight
of250px
. I feel like this should be simple, but everything I tried failed. Some was problems with extra spaces where I couldn’t have them. (This needs to be void of spaces:.accordion.ac-accordion-1905655034
) And some of it was an inability to properly get the value of myheight
parameter to show up. I’m feeling like all this was simple lapses on my part. I have a version that works. Can someone show me how to do it better? -
Does anyone know how to update the CSS transitions so that a hidden panel has
scroll: hidden
until the transition is complete and it getsscroll: auto
. (I haven’t done any real research on this one yet. Please don’t do any digging on my behalf, but if you happen to know, I’d appreciate it if you could share.) -
More generally, what other things should be improved in this code? Where am I doing things the hard war, or the ill-performant way? Where am I missing Tiddlywiki best practices. (E.g., should I be displaying captions if they exist and titles otherwise?) Am I really going off the rails by not using the State mechanism? Etc.