Blog-Archiv

Montag, 20. Juli 2015

CSS Fixed Table Header

There ain't no web developer that never worried about a table with a fixed header. HTML browsers should implement this out of the box. But they don't. We need to do it.

Here is a normal HTML table with a height restricted to 12 em.

OneTwoThree
1 ApplesOrangesPlum
2 ApplesOrangesPlum
3 ApplesOrangesPlum
4 ApplesOrangesPlum
5 ApplesOrangesPlum
6 ApplesOrangesPlum
7 ApplesOrangesPlum
8 ApplesOrangesPlum
9 ApplesOrangesPlum
10 ApplesOrangesPlum
11 ApplesOrangesPlum
12 ApplesOrangesPlum
13 ApplesOrangesPlum
14 ApplesOrangesPlum
15 ApplesOrangesPlum
16 ApplesOrangesPlum
17 ApplesOrangesPlum
18 ApplesOrangesPlum
19 ApplesOrangesPlum
20 ApplesOrangesPlum

Here is the same with fixed header.

One
Two
Three
1 ApplesOrangesPlum
2 ApplesOrangesPlum
3 ApplesOrangesPlum
4 ApplesOrangesPlum
5 ApplesOrangesPlum
6 ApplesOrangesPlum
7 ApplesOrangesPlum
8 ApplesOrangesPlum
9 ApplesOrangesPlum
10 ApplesOrangesPlum
11 ApplesOrangesPlum
12 ApplesOrangesPlum
13 ApplesOrangesPlum
14 ApplesOrangesPlum
15 ApplesOrangesPlum
16 ApplesOrangesPlum
17 ApplesOrangesPlum
18 ApplesOrangesPlum
19 ApplesOrangesPlum
20 ApplesOrangesPlum

Don't see no difference? It's not always the look, sometimes it's the feel :-)
You feel the difference as soon as you scroll down. The fixed header stays at its position while the table content scrolls under it. The non-fixed header scrolls away with the content and is no more visible.

How to achieve this? We now enter the kingdom of absolute and relative positioning :-)

Relative and Absolute Positioning

When you explored my latest Blog you can skip this chapter.

An HTML element that was positioned by CSS position: relative (or absolute, or fixed) is the point of reference for its children with position: absolute.

An HTML element that was positioned by CSS position: absolute will position itself relative to its nearest parent with a position different to static. The relative position is then given by top, left, right, bottom coordinates.

Fix the Header

How can this knowledge be applied to fix a table header?
We need ...

  1. ... a parent div with position: relative that will be the reference point for the absolutely positioned header

  2. ... another parent div to restrict the table-content to a certain height, and restrict the content's overflow to auto, else it won't have a scrollbar (default overflow is visible)

  3. ... to set the header to position: absolute and give it a top: 0 coordinate (default coordinate would be its static position below or beside its predecessor HTML element)

Sounds simple and logical. The header would then go absolutely to its relative parent and stay there, while the table scrolls under it.

Unfortunately the browser's table implementations make it impossible to tear the header cells out of the table and pull it up onto some siding.
And, fortunately, clever people have found a workaround for that:

  • wrap the content of the th header cells into div elements, and
  • position the div absolutely (instead of the th cells).

So, step by step now.

1. The Header Parent

.headerSiding {
    position: relative; /* need a non-static position */
    padding-top: 1.4em; /* place for the fixed header */
}

The topmost parent is positioned relatively, to be the reference point for absolutely positioned child elements. It prepares a siding for the header by declaring a padding. (This padding might need adjustment for headers that are multi-line.)

  <div class="headerSiding">
    ....
  </div>

2. The Scroll Pane

.scrollPane {
    height: 20em; /* without height no scrollbar ever */
    overflow: auto; /* show scrollbar when needed */
}

This is a standard scroll pane. By setting the height, and setting overflow: auto, you force scrollbars when the content does not fit into the height. Mind that this is positioned static, NOT relative, else the scrollPane would be the reference point for the table header, not the headerSiding!

  <div class="headerSiding">
    <div class="scrollPane">
      ....
    </div>
  </div>

3. The Header Cells

.scrollPane th div {
    position: absolute; /* pinned to next non-static parent */
    top: 0; /* at top of parent */
}

This is the tricky part, nevertheless very short. Simply tell the div elements containing the header cell content to go to top, where a padding was prepared for them. That's all concerning CSS, now we also need to look at the HTML.

<div class="headerSiding">
 <div class="scrollPane">

  <table>
    <thead>
      <tr>
        <th><div>One</div></th><th><div>Two</div></th><th><div>Three</div></th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td>1 Apples</td><td>Oranges</td><td>Plum</td>
      </tr>
      <tr>
        <td>2 Apples</td><td>Oranges</td><td>Plum</td>
      </tr>
      ....
    <tbody>
  </table>

 </div>
</div>

This shows the wrapping of the th header cell contents into div elements.


No more tricks needed for this. This is the layout part. One of the few pure CSS soutions I can believe in. I also tried this out on complex pages with further relative and absolute parents, it works everywhere.

You can view this example also on my hompage.

Click here to see full source code.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<!DOCTYPE HTML>
<html>

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
  
    <title>Fixed table header</title>
    
    <style type="text/css">
      .headerSiding  {
        position: relative; /* need a non-static position */
        padding-top: 1.4em; /* place for the fixed header */
      }
      .scrollPane {
        height: 20em; /* without height no scrollbar ever */
        overflow: auto; /* show scrollbar when needed */
      }
      .scrollPane th div  {
        position: absolute; /* pinned to next non-static parent */
        top: 0; /* at top of parent */
      }
    </style>
        
</head>
  
<body style="margin-left: 25%; margin-right: 25%;"> <!-- center contents -->

  <h1>Table with CSS-only Fixed Header</h1>
    
  <div class="headerSiding">
  
    <div class="scrollPane">
    
      <table>

        <thead>
          <tr>
            <th><div>One</div></th><th><div>Two</div></th><th><div>Three</div></th>
          </tr>
        </thead>
        
        <tbody>
          <tr>
            <td>1 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>2 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>3 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>4 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>5 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>6 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>7 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>8 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>9 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>10 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>11 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>12 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>13 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>14 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>15 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>16 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>17 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>18 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>19 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>20 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>21 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>22 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>23 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>24 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>25 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>26 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
          <tr>
            <td>27 Apples</td><td>Oranges</td><td>Plum</td>
          </tr>
        </tbody>
                
      </table>
      
    </div>
  
  </div>
  
</body>

</html>



Sonntag, 19. Juli 2015

CSS Position Property

I believe it is a common sense that the CSS layout properties are not as easy to understand and apply as for example color and border. This is due to the fact that a rule-language like CSS is not the adequate means to specify layout. But anyway it is used for it, and we have to live with a property like position. Even when the display property is of same importance, I will focus only on position in this Blog. Here are its possible values:

position: static;
position: relative;
position: absolute;
position: fixed;

Static is Default

position: static; is the default setting when no position was given. It means that block elements are placed below each other, and inline elements beside each other. In other words, everything is like we know it from web browsers.

It makes no sense to define top, left, right and bottom coordinates on a static element, they will simply be ignored.

Relative Meanings

position: relative; has two meanings:

  1. it makes the top, left, right and bottom properties available for the element (which would be ignored by static positioning)
  2. it marks an element as parent for absolute positioning of child elements

In practice this means that you can relatively adjust such an element by using top, left, right and bottom, but more frequently such positioning means that the element is used as parent for absolutely positioned child elements.

Absolute is Relative

The "Oops" effect when you find out that position: absolute; means positioning relatively to the parent element. But not to any parent element, only to a non-static parent element!

In other words, you pin the absolute element to a non-static parent and then position it using top, left, right and bottom. It will always stay there, even when other children might scroll away.

This relative/absolute pairing also works recursively. Useful e.g. for a fixed table header. Mind that the top, left, right and bottom coordinates are relative to the HTML page, not to the browser view-port.

There are two more things to say about absolutely positioned elements:

  1. a block element does not expand to 100% width any more, it expands just to the width its content (text) demands
  2. the element goes above static elements (in z-direction), in a way that it will overlap them, because additionally static elements behave as it would not be there any more

Fixed relatively to Browser, not to Page

When you want to have a button that always stays in some corner of the browser client window, then you must use a position: fixed; assignment. This pins the element to the browser view-port, and it won't scroll when the browser content is scrolled. Of course you can also use top, left, right and bottom here.

Very suitable also for dialog windows that should appear relative to the browser window, and not relative to the (possibly much bigger) page content.


Sandbox

We need to play around with this to understand it. I created a test page that provides several levels of nested div elements. You can use it for testing by setting position, top, left, right, bottom properties to these elements, and see where it goes then.

Click to see the HTML structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Absolute and Relative Positioning</title>
    
    <style type="text/css">
      #level-0 {
        background-color: yellow;
        height: 22em;
      }
      #level-1 {
        background-color: orange;
        height: 19.5em;
      }
      #level-2 {
        background-color: magenta;
        height: 17em;
      }
      #level-3_1, #level-3_2 {
        height: 7em;
      }
      #level-4_1, #level-4_2 {
        height: 2em;
      }
      #level-3_1 {
        background-color: cyan;
      }
      #level-3_2 {
        background-color: SkyBlue;
      }
      #level-4_1 {
        background-color: PaleGreen;
      }
      #level-4_2 {
        background-color: LawnGreen;
      }
    </style>
</head>
  
<body>

  <div style="margin-left: 10%; margin-right: 10%; max-height: 25em; overflow: auto; border: 1px dotted black;">
  
    <div id="level-0">
      Level 0
    
      <div id="level-1">
        Level 1
        
        <div id="level-2">
          Level 2
          
          <div id="level-3_1">
            Level 3 / 1
            
            <div id="level-4_1">
              Level 4 / 1
            </div>
            
            <div id="level-4_2">
              Level 4 / 2
            </div>
            
          </div>
          
          <div id="level-3_2">
            Level 3 / 2
          </div>
            
        </div>
        
      </div>
      
    </div>

  </div>

</body>
</html>

This colorful test page needs some explanations.

  • You have 5 levels of nested HTML div elements (see source above), each has another color. You can change CSS properties of any div by using the according properties table below.

  • When positioning the yellow base div to absolute, content from below will be raised and might appear under it (in z-order), or under the properties panel, because an absolute positioned element plays no more role in position calculations for static elements. The property settings panel might be obscured then, this is what the "Pin Properties Panel" checkbox is for: it positions the panel absolutely to its current x-coordinate, and sets its z-index to 2 (thus it should always be available to repair settings).

  • In case you can not change settings any more, make a page reload. Be aware that the Firefox browser remembers the settings of input fields even across a page reload (needs Shift-Reload).

  • When you choose fixed positioning, the element will disappear. You will see it again when you set top to e.g. 1. Watch then that you can scroll the page content below it, the fixed element won't move.

There might be some more comments needed here, but maybe you'll discover everything on your own now. Mind that in most browsers the top and left properties overrule right and bottom. You can not size the element by using e.g. both left and right.

Faites vos sable jeux, s'il vous plaît :-)



Level 0
Level 1
Level 2
Level 3 / 1
Level 4 / 1
Level 4 / 2
Level 3 / 2

CSS Style Properties

Pin Properties Panel
Level 0
position:
height:
width:
top:
left:
right:
bottom:
Level 1
position:
height:
width:
top:
left:
right:
bottom:
Level 2
position:
height:
width:
top:
left:
right:
bottom:
Level 3 / 1
position:
height:
width:
top:
left:
right:
bottom:
Level 4 / 1
position:
height:
width:
top:
left:
right:
bottom:
Level 4 / 2
position:
height:
width:
top:
left:
right:
bottom:
Level 3 / 2
position:
height:
width:
top:
left:
right:
bottom:




Sonntag, 12. Juli 2015

JS Animated Expand and Collapse

Text folding is nice, but nowadays anything has to be animated. This is a matter of ergonomics. Because animations are mostly done using CSS, I tried to find a pure CSS solution for this. You can follow them trying on Stackoverflow. None of these solutions manage to come across the fact that the CSS transition needs a hard-coded height value. Now do this for a paragraph whose content changes every week, and for devices of all size!

If you want to hack on it, here is some HTML code (for copy/paste) that demonstrates that inability.

Click to see source (animated:-).
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
  
    <title>Pure CSS Height Transition</title>
    
    <style type="text/css">
      button:active + p {
        height: auto;  /* ... "auto" is not animated, would need hardcoded height! */
      }
      p {
        height: 0;
        overflow: hidden;
        transition: 1s;
      }
    </style>
  </head>
  
  <body>
    
    <button>Click to Expand Text</button>
    
    <p>
      Put some very long text to here ...
    </p>
      
  </body>
</html>

Before trying this out you should put some Lorem Ipsum (long text) into the p element, else the animation will be hard to see.

The best workaround I found was one that recommended to use max-height and not height, and put an astronomic value to max-height in expanded state. Result is that the animation is also "astronomic" :-)

My advice for CSS ambitions is:

Do not go for a pure CSS solution when it does not work after a short time of trying.
This Blog is about an animated expand/collapse solution using JavaScript.


Specification

My user stories for this are:

As a browser user, ....

  • ... I do not want to initially see unimportant things on a web page
  • ... I want to see the unimportant things by clicking on some expansion-arrow
  • ... I want the expansion to happen ergonomically, i.e. the transition should be slow, at about 1 second

Another kind of user stories are programmer stories. Because any solution needs programmers to be integrated into an application, the API of the solution needs to be simple but precise, and safe against abuse. This is a matter of software survival!

As a web programmer, ....

  • ... I want to write as less code as possible into the web page to achieve animated expand/collapse
  • ... I do not want to reference lots of different resources (arrow images, CSS files, JS files, ...)
  • ... it should not be necessary to implement a complicated initialization to make this working

API Design

Here is the HTML shape I want to achieve.

    <div toggle-id="one">Reveal Paragraph One</div>
    <p id="one">Some long long text here ...</p>

    <p toggle-id="two">Reveal Division Two</p>
    <div id="two">Some long long text here ...</div>

It should be possible to use any element type as expansion trigger, and it should be possible to use any type of element for the expansion target. In the example above, the first expansion trigger is a div and the target is a p, while in the second example it is the other way round.

The expansion trigger declares its target in an attribute called toggle-id, while the target wears the value referenced there in its id attribute. Nothing more is needed, anything else (arrows) can be done by the script. So that's the API to satisfy the programmer stories.

JS Implementation

The script should be a reusable module, concentrating on what's to do and nothing else. I will introduce it piece by piece, and then provide the whole source on bottom of the page.

Initialization

Following is the start of a factory-function that will be continued by snippets across following chapters.

      var expanderFactory = function(initiallyExpanded)
      {
        var that = {};
        var toggleSelector = "*[toggle-id]";
        
        that.install = function() {
          var toggles = document.querySelectorAll(toggleSelector);
          for (var i = 0; i < toggles.length; i++)
            initialize(toggles[i]);
          
          var style = document.createElement("style");
          document.head.appendChild(style);
          var sheet = style.sheet;
          sheet.insertRule(toggleSelector+".collapsed::before { content: '\u25B6 '; }", 0);
          sheet.insertRule(toggleSelector+".expanded::before { content: '\u25BC '; }", 1);
        };

        var initialize = function(toggle) {
          toggle.style.cursor = "pointer";
          toggle.className += initiallyExpanded ? " expanded" : " collapsed";
          toggle.addEventListener("click", function() {
            doToggle(toggle);
          });
          
          var target = getToggleTarget(toggle);
          target.myOriginalHeight = target.clientHeight;
          target.style.height = initiallyExpanded ? target.myOriginalHeight+"px" : "0";
          target.style.overflow = "hidden";
          target.style.transition = "height 0.5s";
        };
 

This code provides a public install() function that should be called on page-load. It searches for elements wearing the toggle-id attribute and initializes them.

After that it creates a CSS style element in the HTML head, where it declares two rules. One is a pseudo-element for the CSS class expanded, the other a pseudo-element for the CSS class collapsed. These are the expansion arrows.
I could also have done this by DOM manipulations, but as it is only a few lines of code, I did it via CSS.

The private initialize() function defines the hand-cursor upon the toggle-element (expansion trigger). It then sets the according CSS class on it to generate the expand- or collapse-arrow via pseudo-element. Then it installs a click-listener that calls the doToggle() function when the user clicks on it.

Next it does some size management. This works only when the page already has been rendered. It retrieves the target element of the toggle, and stores the original height of the element (target.clientHeight) into a new attribute called myOriginalHeight. (In JavaScript, unlike in Java, it is possible at any time and on every object to create a new attribute.) This will be the height used to expand the element. It then sets overflow: hidden, else the element would show despite of height 0. Finally it declares the animation via a CSS transition of 1 second on property height.

Event Callback

        var getToggleTarget = function(toggle) {
          var targetId = toggle.getAttribute("toggle-id");
          return document.getElementById(targetId);
        };
        
        var doToggle = function(toggle) {
          var wasExpanded = /\bexpanded\b/.test(toggle.className);
          var target = getToggleTarget(toggle);
          target.style.height = wasExpanded ? "0" : target.myOriginalHeight+"px";
          toggle.className = toggle.className.replace(
              wasExpanded ? /\bexpanded\b/ : /\bcollapsed\b/,
              wasExpanded ? "collapsed" : "expanded");
        };

This is the event handling when the user clicks on the expansion trigger (toggle). The getToggleTarget() function fetches the target element by calling the document.getElementById() function, using the value of the toggle-id attribute.

The doToggle() function then does the work. First it finds out whether the eleent is currently expanded or collapsed by searching for the according CSS class. It then toggles the height of the arget element to either its original or zero height. Then it replaces the CSS class by its counterpart, to store the expansion state of the element.

Instantiation, Installation

        
        return that;
        
      };
      
      var expander = expanderFactory(true); /* initiallyExpanded */
      window.addEventListener("load", expander.install);
 

This is the end of the factory function, and the concrete instatiation of an expander-object from it. Passing a value of true would cause all text to be expanded.

This code then registers the expander's install() function to be called on the browser's on-load event, when the page will be already rendered and all heights are calculated. These two lines are meant to be in a <script> tag anywhere in the HTML page when the factory-function is loaded as module from a JS-file.

Here you can see the script in action (click on this text, or the left-side arrow). It reveals its own source code, wrapped in an example HTML. This source also contains functions to perform a bulk toggle of all expansion-elements in the page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
  
    <title>JS animated expand/collapse</title>
  </head>
  
  <body>
    <div toggle-id="one">Reveal One</div> 
    <p id="one">I am One<br>I am One<br>I am One<br>I am One<br>I am One<br></p>
    
    <p toggle-id="two">Reveal Two</p> 
    <div id="two">I am Two<br>I am Two<br>I am Two<br>I am Two<br>I am Two<br>I am Two<br></div>
    
    
    <script type="text/javascript">
      var expanderFactory = function(initiallyExpanded)
      {
        var that = {};
        var toggleSelector = "*[toggle-id]";
        
        var getToggleTarget = function(toggle) {
          var targetId = toggle.getAttribute("toggle-id");
          return document.getElementById(targetId);
        };
        
        var doToggle = function(toggle) {
          var wasExpanded = /\bexpanded\b/.test(toggle.className);
          var target = getToggleTarget(toggle);
          target.style.height = wasExpanded ? "0" : target.myOriginalHeight+"px";
          toggle.className = toggle.className.replace(
              wasExpanded ? /\bexpanded\b/ : /\bcollapsed\b/,
              wasExpanded ? "collapsed" : "expanded");
        };
        
        var toggleAll = function(expand) {
          var toggles = document.querySelectorAll(toggleSelector);
          for (var i = 0; i < toggles.length; i++)
            if (expand != /\bexpanded\b/.test(toggles[i].className))
              doToggle(toggles[i]);
        };
        
        var initialize = function(toggle) {
          toggle.style.cursor = "pointer";
          toggle.className += initiallyExpanded ? " expanded" : " collapsed";
          toggle.addEventListener("click", function() {
            doToggle(toggle);
          });
          
          var target = getToggleTarget(toggle);
          target.myOriginalHeight = target.clientHeight;
          target.style.height = initiallyExpanded ? target.myOriginalHeight+"px" : "0";
          target.style.overflow = "hidden";
          target.style.transition = "height 0.5s";
        };
        
        that.install = function() {
          var toggles = document.querySelectorAll(toggleSelector);
          for (var i = 0; i < toggles.length; i++)
            initialize(toggles[i]);
          
          var style = document.createElement("style");
          document.head.appendChild(style);
          var sheet = style.sheet;
          sheet.insertRule(toggleSelector+".collapsed::before { content: '\u25B6 '; }", 0);
          sheet.insertRule(toggleSelector+".expanded::before { content: '\u25BC '; }", 1);
        };
        
        that.expandAll = function() {
          toggleAll(true);
        };
        
        that.collapseAll = function() {
          toggleAll(false);
        };
        
        return that;
        
      };
      
      var expander = expanderFactory(true); /* initiallyExpanded */
      window.addEventListener("load", expander.install);
      
    </script>
    
  </body>
</html>

Available also on my homepage.