JS Folding


Published: 2015-02-10
Updated: 2015-07-10
Web: https://fritzthecat-blog.blogspot.com/2015/02/js-folding.html


Web pages offer more and other possibilities than print pages. One is folding. It can hide text that may not be important on first glance. But it can also serve to implement expansible trees.

You can connect an expansion trigger to something to expand. Click on the left-side triangle to see how this is done.

The JavaScript used in this "Disclosure" exposes a function called toggle() that

That function can be called by any element's onclick event listener, with (1) the expand-element itself and (2) the content-element to toggle as parameters.

A span element serves as expand-element here, holding the expand-symbol, the onclick event listener, and style="cursor: pointer" to show the hyperlink-cursor on mouse-over. To reference the content-element, the global document.getElementById() function is used. All that remains to do now is set the referenced id on the content-element as id="expand_example".

Mind: folded text will not be found by a page text search!

CSS Expand Control

Here are some JS statements to define expand controls.

  var blackTriangleRight = "\u25B6";
var blackTriangleDown = "\u25BC";
var whiteTriangleRight = "\u25B7";
var whiteTriangleDown = "\u25BD";
var blackTriangleRightSmall = "\u25B8";
var blackTriangleDownSmall = "\u25BE";
var whiteTriangleRightSmall = "\u25B9";
var whiteTriangleDownSmall = "\u25BF";
var plus = "+";
var minus = "\u2012";

And here is their HTML representation.

▶▼
▷▽
▸▾
▹▿
++‒

But it is boring to write that in HTML. You could also prepend it ....

.... using JS and CSS classes with pseudo-elements (click here to see how this is done)!

That means, you set or reset a CSS class named expanded on every click, which causes the CSS ::before pseudo-element rules to toggle the expand-control.

The biggest problem is the addressing of the content-element. It would be nice to have another technique than id references. For which I see two solutions: connecting the expansion-trigger ...

  1. to the next sibling (the trigger having the same parent as the content)
  2. to a single nested child element (so the trigger being the parent of the content)

The first one is nice for disclosures. The second is nice for trees.

Disclosures, Stack Panels, Accordions

Web development has created a lot of names for UI controls. Disclosures, Stack Panels, and Accordions do nearly all the same. You could regard Accordion and Stack Panel to be a list of Disclosures that allow just one "fold" open at a time.

Click on this text to open a JS solution for disclosures.

 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
<!DOCTYPE html>
<html>

<head>
<style type="text/css">
.expandControl {
cursor: pointer;
}
.expandControl::before {
content: "\025B7";
}
.expandControl.expanded::before {
content: "\025BD";
}
</style>
</head>

<body>

<p class="expandControl">
Click on the left-side triangle to open a JS solution for Disclosures.
</p>

<div>
Content 1 goes here ....
</div>


<script type="text/javascript">
var toggle = function(trigger, content) {
if (content.style.display === '') {
content.style.display = 'none';
trigger.className = "expandControl";
}
else {
content.style.display = '';
trigger.className = "expandControl expanded";
}
};

var init = function(root) {
root = root || document.documentElement;
var expandControls = root.getElementsByClassName("expandControl");

for (var i = 0; i < expandControls.length; i++) {
var expandControl = expandControls[i];
var content = undefined;

// find next sibling - quite difficult with the w3c DOM API!
var parent = expandControl.parentNode;
var children = parent.children;
var previous = undefined;
for (var j = 0; j < children.length && ! content; j++) {
var element = parent.children[j];
if (previous === expandControl)
content = element;
else
previous = element;
}

if (content)
connect(expandControl, content);
}
};

var connect = function(expandControl, content) {
expandControl.addEventListener("click", function(event) {
event.stopPropagation(); // needed to prevent ancestor items to also receive that click
toggle(expandControl, content);
});
content.style.display = "none";
};

init();

</script>

</body>
</html>

This is really smart. No id references, no onclick listeners, you just have to set the expandControl CSS class on each expansion trigger, the JS init() function does the rest. Maybe it should output a warning if it did not find any follower sibling for some expand-control.

Trees

For trees it is different. Every LI element is an expand-control, thus we don't want to set the CSS class manuallly on each of them. Further there should be an triangle only on elements that actually contain sub-lists. When not, an indentation placeholder must be there instead.

It does not make much sense to turn a numbered list into a tree. So I restrict this to UL lists (unnumbered lists).

Here is an example list with sub-lists that I want to turn into an expansible tree:

Open this disclosure to see a JS solution for trees on nested lists.

 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
<!DOCTYPE html>
<html>

<head>
<style type="text/css">
ul {
list-style-type: none;
padding-left: 1em;
}
ul li.expandControl::before {
content: "\025B7";
cursor: pointer;
padding-right: 0.4em;
}
ul li.expandControl.expanded::before {
content: "\025BD";
cursor: pointer;
padding-right: 0.4em;
}
ul li.expandControlEmpty::before {
content: "\020";
padding-left: 1.2em;
}
</style>
</head>

<body>

<ul>
<li>item 1
<ul>
<li>item 1.1</li>
<li>item 1.2</li>

<li>item 1.3
<ul>
<li>item 1.3.1</li>
<li>item 1.3.2</li>
<li>item 1.3.3</li>
</ul>
</li>

<li>item 1.4</li>
</ul>
</li>

<li>item 2</li>
</ul>


<script type="text/javascript">
var toggle = function(trigger, content) {
if (content.style.display === '') {
content.style.display = 'none';
trigger.className = "expandControl";
}
else {
content.style.display = '';
trigger.className = "expandControl expanded";
}
};

var init = function(root) {
root = root || document;
var expandControls = root.getElementsByTagName("LI");

for (var i = 0; i < expandControls.length; i++) {
var expandControl = expandControls[i];
// find child list
var children = expandControl.children;
var subList = (children.length === 1 && children[0].tagName === "UL")
? children[0]
: undefined;
connect(expandControl, subList);
}
};

var connect = function(expandControl, content) {
expandControl.className = content ? "expandControl" : "expandControlEmpty";
expandControl.addEventListener("click", function(event) {
event.stopPropagation(); // needed to prevent ancestor items to also receive that click
if (content)
toggle(expandControl, content);
});

if (content)
content.style.display = "none";
};

init();

</script>

</body>
</html>

The init() JS function fetches all LI elements and connects them with their nested UL list, when existing. Because browsers pass unconsumed events to parent elements ("event bubbling"), the click event must also be caught on empty LI items, otherwise the parent LI would answer to the mouse event by collapsing.

And here is how the the list above looks (and acts) after having converted it to a tree:



Feel free to visit my homepage to see more advanced JS code about disclosures and trees. On bottom of the page you will have the full highlighted JS source in disclosures :-)





ɔ⃝ Fritz Ritzberger, 2015-02-10