Expand/Collapse text bug - starts in open state

I am having problems with collapsible/expandable links. When the web page first opens, the links should be iin a closed or collapsed state. Click the link once and the text opens directly below it. Click it again and the link closes (the text goes away below the link). I’m using this toggle javascript:

<script type=“text/javascript”>
function toggleObj(obj,init,lshow,lhide,swap,set,cname) {
var Ar = new Array(obj,init,lshow,lhide,swap,set,cname);
var elstyle = document.getElementById(obj).style;
if (Ar[4]) var swstyle = document.getElementById(swap).style;
var text = document.getElementById(obj + “-tog”);
if (Ar[1]==‘hide’) { //hide object
if (Ar[5]==‘1’) document.cookie=Ar[6]+‘=hide; path=/’;
Ar[1]=‘show’;
elstyle.display = ‘none’;
if(Ar[4]) swstyle.display = ‘block’;
copy=‘<a class=“wikilink” ‘;
copy+=‘href="javascript:toggleObj(\’’+Ar[0]+’\‘,\’‘+Ar[1]+’\‘,\’‘+Ar[2]+’\‘,\’‘;
copy+= Ar[3]+’\‘,\’‘+Ar[4]+’\‘,\’‘+Ar[5]+’\‘,\’‘+Ar[6]+’\‘);">’+Ar[2]+‘</a>’;
text.innerHTML = copy;
}
else if (Ar[1]==‘show’) { //show object
if (Ar[5]==‘1’) document.cookie=Ar[6]+‘=show; path=/’;
Ar[1]=‘hide’;
elstyle.display = ‘block’;
if(Ar[4]) swstyle.display = ‘none’;
copy=‘<a class=“wikilink” ‘;
copy+=‘href="javascript:toggleObj(\’’+Ar[0]+’\‘,\’‘+Ar[1]+’\‘,\’‘+Ar[2]+’\‘,\’‘;
copy+= Ar[3]+’\‘,\’‘+Ar[4]+’\‘,\’‘+Ar[5]+’\‘,\’‘+Ar[6]+’\‘);">’+Ar[3]+‘</a>’;
text.innerHTML = copy;
}
}
</script>

Then, in the body of the html, I have something like:

<li><span id=“lac10-tog” class=“toggle”><a class=“wikilink” href=“javascript:toggleObj(‘lac10’,‘show’,
‘Link to be clicked’,‘Link to be clicked’,‘’,‘0’,‘’)” >Link to be clicked</a></span>
<style type=‘text/css’>
#lac10 { display:none; }
</style>
</li>
<div class=‘hidediv faqanswer’ id=‘lac10’>
<p>text that expands and collapses below the clicked link</p> </div>

The problem is that when the number of expandable links reaches 20 or so, the links start opening in the expanded state instead of the collapsed state. I want the links to always start out in the collapsed state. Interestingly, the whole thing breaks only when it’s an shtml page, and html doesn’t seem to cause problems.Sometimes, it only breaks when we upload it to the server. Any ideas? Thanks. Polly

Whoa! That’s a lot of code to show/hide some div …

Try it like this


<a href="javascipt:void(0);" style="display:block;" onclick="toggle('hidediv');">Click here!</a>
<div id="hidediv">
   ... div contents ...
</div>
<script language="javascript" type="text/javascript">// <![CDATA[
  document.getElementById("hidediv").style.display="none";
  function toggle(elem) {
    var e = document.getElementById("hidediv");
    e.style.display = (e.style.display == "block" ? "none" : "block");
  }
//]]></script>

That initially shows the div, and then hides it through javascript. People that have a browser that doesn’t support javascript always see the links.
i.e., you’re not “punishing” them for having no javascript support.

PS. style=“display:block” needs to be there, otherwise the code won’t work (at least not in all popular browser). If you want to hide the div initially make it style=“display:none”

Thanks for your suggestions. I tried them and got the “Click here!” to display. When I clicked it, I saw the “div” show up briefly but then another web page came up saying the protocol couldn’t be found. Any idea what is wrong? Thanks. Polly

Let’s get rid of some anachronisms from the code while we’re as it :slight_smile:

These are all minor improvements, but all together they add up to quite a change.

[list][]The link shold not use that javascript expression for its href. To prevent a link from being followed, we can return false from its onclick event instead.
[
]The inline onclick event is a hanger-on from the bad-old days of inline css and inline scripting. Get rid of it, and use scripting to attach the event on to the link instead.
[]The inline css style can go too. What should be used instead is a block-level element such as a paragraph.
[
]The link should not even be in with the HTML content itself. Instead, the script should create it, as it will only be useful when scripting is available.
[]The language attribute was of no use when HTML 4 was created. It is long past the time to stop using it.
[
]The XHTML CDATA parts are only relevant to inline scripting. We should not be using inline scripting, so place the script in an external file instead.
[*]The script itself should be placed just before the </body> tag, so that it can easily manipulate the page elements, and load the page faster.[/list]
That leaves us with the following body code:


<body>
...
    <div id="hidediv">
       ... div contents ...
    </div>
    ...
    <script type="text/javascript" src="js/script.js"></script>
</body>

Let’s add the click button now.

  • Instead of calling it addClickHere, let’s give it a more generic name of addToggleLink so that we can easily pass the message in to it as well.

js/script.js


var el = document.getElementById("hidediv");
addToggleLink(el, 'Click here!');

function addToggleLink(el, message) {
    var p = document.createElement('p');
    var a = document.createElement('a');
    var text = document.createTextNode(message);
    a.href = '#';
    a.appendChild(text);
    a.onclick = function () {
        toggle(el);
        return false;
    }
    p.appendChild(a);
    el.toggle = a;
    el.parentNode.insertBefore(p, el);
}

We have even assigned to the div a property called toggle, which refers to the link itself so that we can easily gain access to it.

  • The toggle function should not fiddle around with styles directly. Instead it should add/remove class names.

This example will set the class name directly, but to handle multiple class names correctly, here are some good hasClass, addClass, and removeClass functions.

The class name that we’ll add and remove is called “hidden”

css/style.css


.hidden {
    display: none;
}

  • The toggle function should not require knowledge of “hidediv”

The toggle function also accepts the elem parameter but doesn’t use it, so let us now make use of it.

As we want the div to be initially hidden, we can easily do that by triggering the onclick event of the link.


el.toggle.onclick();
function toggle(el) {
    el.style.display = (el.style.display == "block" ? "none" : "block");
}

As we want the div to be initially hidden, we can easily do that by triggering the onclick event of the link.
function toggle(el) {
el.style.display = (el.style.display == “block” ? “none” : “block”);
}

Why styles here and not classes? Or is it only faster the first time? I looks like every time toggle is run (run when the onclick event of el’s a is triggered?) it’ll play with the styles instead?

Aargh! I should learn to copy/paste EVERYTHING from my test code.

Please, have a virtual chocolate fish on me, and thanks.

I’ll make a small update to the above code:


...
a.onclick = function () {
    toggleClass(el, 'hidden');
    return false;
}
...

Now we can use a generic class for toggling any class name that we like.


function toggleClass(el, className) {
    if (el.className === className) {
        el.className = '';
    } else {
        el.className = className;
    }
}

And if you decide to make use of the hasClass,addClass, removeClass functions that were mentioned earlier (I seriously suggest that you do), then you can avoid any multiple class name problems as follows:


function toggleClass(el, className) {
    if (hasClass(el, className)) {
        removeClass(el, className);
    } else {
        addClass(el, className);
    }
}

It looks from your code like the “Click here” is embedded in the javascript. Therefore, I couldn’t have a list of links, each expandable/collapsible with text below the link. As I have in the javascript at the top of this thread, the “Click here” is in the HTML, not the javascript, such that I can use it over and over again with different link labels. I have a long list of expandable links I want to create, but the problem is they are starting in the open state with sthml, not with html. (But there are other problems with html in this case.)

Please consider how people with no scripting will use your page.

One option that you have is to hide the “click here” message on the page, so that it’s not confusingly visible to non-scripting users.

That way you can then enable and activate the link when scripting is available.

I have to go elsewhere for an hour or so, but I will be back with a good code update for this.

I would like the “Click here” to display whether or not they have javascript. I would like the stuff inside the div to be hidden when the page first opens if they have javascript. If they don’t have javascript, they should either get a notification in the header to activate it, or the div stuff should be visible and not hidden for these people if they can’t activate javascript. Does that make sense? I’m a novice, so please explain clearly. I really apprreciate your help.
-Polly

Thanks polly, your idea can be easily met too.

The HTML for this example is as follows.


<!DOCTYPE HTML>
<html>
<head>
<title>Test show/hide</title>
<link type="text/css" rel="stylesheet" href="css/style.css">
</head>
<body>
    <p class="hidden"><a href="#hidediv" class="toggle">Custom click here message</a></p>
    <div id="hidediv">
       The contents of the "hidediv" container.
    </div>
<script type="text/javascript" src="js/compat.js"></script>
<script type="text/javascript" src="js/common.js"></script>
<script type="text/javascript" src="js/script.js"></script>
</body> 
</html>

If you do not want the link to be hidden from non-scripting environments, just remove class=“hidden” from the paragraph tag.

Here is the script that works on a wide variety of web browsers, even all the way back to IE 5.5

js/script.js


function activateToggleLinks() {
    var toggles = document.getElementsByClassName('toggle'),
        toggleLinks = Array.prototype.filter.call(toggles, function(el) {
            return el.nodeName === 'A';
        }),
        link,
        hiddenEl,
        i;
    for (i = 0; i < toggleLinks.length; i += 1) {
        // show link if hidden
        hiddenEl = toggleLinks[i];
        while (!hasClass(hiddenEl, 'hidden') && hiddenEl.nodeName !== 'BODY') {
            hiddenEl = hiddenEl.parentNode;
        }
        removeClass(hiddenEl, 'hidden');
    
        // attach toggle event
        toggleLinks[i].onclick = function () {
            var id = this.href.split('#')[1];
            toggleClass(document.getElementById(id), 'hidden');
            return false;
        }
    }
}
activateToggleLinks();

The Array.prototype.filter.call method is used because the elements might be returned as a nodeList, or an array, depending on whether the browser supports a native implementation of getElementsByClassName or not.

As you can imagine, some support code is also required. The class handling functions have been placed in js/common.js

js/common.js


function hasClass(ele,cls) {
	return ele.className.match(new RegExp('(\\\\s|^)'+cls+'(\\\\s|$)'));
}
 
function addClass(ele,cls) {
	if (!this.hasClass(ele,cls)) ele.className += " "+cls;
}

function removeClass(ele,cls) {
	if (hasClass(ele,cls)) {
    	var reg = new RegExp('(\\\\s|^)'+cls+'(\\\\s|$)');
		ele.className=ele.className.replace(reg,' ');
	}
}

function toggleClass(el, cls) {
    if (hasClass(el, cls)) {
        removeClass(el, cls);
    } else {
        addClass(el, cls);
    }
}

The hasClass, addClass, and removeClass functions are from http://snipplr.com/view/3561/addclass-removeclass-hasclass/

Finally there is compatibility code so that non-modern web browsers can enjoy the ability to getElementsByClassName and to filter array contents.

js/compat.js


/*
	Written by Jonathan Snook, http://www.snook.ca/jonathan
	Add-ons by Robert Nyman, http://www.robertnyman.com
*/
function getElementsByClassName(oElm, strTagName, strClassName){
	var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
	var arrReturnElements = new Array();
	strClassName = strClassName.replace(/\\-/g, "\\\\-");
	var oRegExp = new RegExp("(^|\\\\s)" + strClassName + "(\\\\s|$)");
	var oElement;
	for(var i=0; i<arrElements.length; i++){
		oElement = arrElements[i];
		if(oRegExp.test(oElement.className)){
			arrReturnElements.push(oElement);
		}
	}
	return (arrReturnElements);
}

// Add getElementsByClassName where a native implementation is not available
if (!document.getElementsByClassName) {
    Object.prototype.getElementsByClassName = document.getElementsByClassName = function (className) {
        return getElementsByClassName(this, '*', className);
    }
    
}

// Add filter method for arrays if not available
if (!Array.prototype.filter) {  
    Array.prototype.filter = function(fun /*, thisp*/) {  
        var len = this.length >>> 0;  
        if (typeof fun != "function") {
            throw new TypeError();
        }
        var res = [];  
        var thisp = arguments[1];  
        for (var i = 0; i < len; i++) {  
            if (i in this) {  
                var val = this[i]; // in case fun mutates this  
                if (fun.call(thisp, val, i, this)) {
                    res.push(val);
                }
            }  
        }
        return res;  
    };  
}  

The getElementsByClassName function is from http://robertnyman.com/2005/11/07/the-ultimate-getelementsbyclassname/
The Array.filter method is from https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter

Paul, thank you so much for your careful documentation. This looks perfect! I will try it out tomorrow and let you know. -Polly

Paul, I ended up checking it out tonight and it works beautifully, except for one thing. It toggles between expanded and collapsed just fine, but it starts in the expanded or open state. I would like it to start in the closed or collapsed state and then toggle open and closed from there. I imagine it’s an easy fix. Can you tell me what to change? Thanks.

There are two different types of situations that might apply here.

  1. All toggled items are to start as closed

For this you can trigger the onclick event after it has been assigned


// attach toggle event
toggleLinks[i].onclick = function () {
    var id = this.href.split('#')[1];
    toggleClass(document.getElementById(id), 'hidden');
    return false;
}

// hide toggled elements
toggleLinks[i].onclick();

  1. The second situation is where you want some toggled elements to start open, and others to start closed.

If we carry on from the above code and start them as normally closed, we can add a second class name to the link, in order to specify that we want it to be treated differently.

Keep in mind that IE6 and below only pay attention to the last class name (for styling purposes) so it pays to leave “toggle” as the second one in the list. That way styles that may be applied to the toggle class will have less chance of being impacted.


<p class="hidden"><a href="#littleBottle" class="active toggle">Drink me!</a></p>
<p class="hidden"><a href="#currantCake" class="toggle">Eat me!</a></p>

Then we can add a condition to the script, so that the linked content will be hidden only if it doesn’t have the “active” class.
This way you can apply the “active” class to just one , or to multiple links on the page.


// attach toggle event
toggleLinks[i].onclick = function () {
    var id = this.href.split('#')[1];
    toggleClass(document.getElementById(id), 'hidden');
    return false;
}

// hide toggled elements
if (!hasClass(toggleLinks[i], 'active')) {
    toggleLinks[i].onclick();
}

Perfect, Paul! Thank you so much for all your help! I really appreciate it!
-Polly

Paul, I’ve encountered a problem. I want all links to start closed and none to start open. So I used the first set of code you gave me with the first attach toggle event and hide toggle elements. What happens is that the first link starts out closed but the link below it starts out open when I open the first link. The second link is embedded within the div for the first link and I would like it to start out closed. Here’s what a section of the html looks like:

<!-- Begin list of bulleted items with expandable content directly below each clicked item –>
<ul>
<li><a href=“#hidediv” class=“toggle”>ASD Court Services, Reduced Supvervision,and Intake</a>
</li>
<div id=“hidediv”>
<p><a href="mailto:bill.f.penny@co.multnomah.or.us"><strong>Bill Penny,</strong></a><strong> District Manager:</strong> 503.988.3014<br />
<br />
The Centralized Intake and Court Services section includes: pretrial release programs at booking and following arraignment; supervision system entry and assessment; local supervisory authority functions units; and, an assortment of specialized services such as drug court supervision, interstate compact, electronic monitoring, violation hearings and investigations programs. Reduced Supervision is also included. The specific units include the following: </p>
<!-- Begin list of nested bulleted items with expandable content directly below each clicked item –>
<ul>
<li><a href=“#hidediv” class=“toggle”>Centralized Intake</a>
</li>
<div id=“hidediv”>
<p><a href="mailto:cate.a.connell@co.multnomah.or.us"><strong>Cate Connell,</strong></a> <strong>Community Justice Manager: </strong>503.988.3458</p></div></div>

You’re making an HTML error that you’ll need to correct anyway:
you may only have one id called “hidediv” on a page. So, you could change them to classes instead. Then make a unique ID for each div to match a unique ID in each anchor… more work I know, but you can assign the href with
a.href = ‘#hide_’ + [i] where i is a number in an array. Your divs can just have them

<a href=“#hide_1”>first link</a>

<div id=“hide_1” class=“hidediv”>

</div>

BTW if those are real email addresses and names, do you want a mod to cover those up? Google reads this forum.

Thanks. That helps a bit. But the first link (top level link) is now start out open even though the embedded link is now closed as it should be when it first appears.

<!-- Begin list of bulleted items with expandable content directly below each clicked item –>
<ul>
<li><a href=“#hide-1” class=“toggle”>ASD Court Services, Reduced Supvervision,and Intake</a>
</li>
<div id=“hide-1” class=“hidediv”>
<p><a href="mailto:bill.f.penny@co.multnomah.or.us"><strong>Bill Penny,</strong></a><strong> District Manager:</strong> 503.988.3014<br />
<br />
The Centralized Intake and Court Services section includes: pretrial release programs at booking and following arraignment; supervision system entry and assessment; local supervisory authority functions units; and, an assortment of specialized services such as drug court supervision, interstate compact, electronic monitoring, violation hearings and investigations programs. Reduced Supervision is also included. The specific units include the following: </p>
<!-- Begin list of nested bulleted items with expandable content directly below each clicked item –>
<ul>
<li><a href=“#hide-11” class=“toggle”>Centralized Intake</a>
</li>
<div id=“hide-11” class=“hidediv”>
<p><a href="mailto:cate.a.connell@co.multnomah.or.us"><strong>Cate Connell,</strong></a> <strong>Community Justice Manager: </strong>503.988.3458</p>
</div></ul></div></ul>

That is a problem due to the nesting and ho the browsers handle hiding elements within already hidden elements.

The code you are using from my example here is not suitable for the use that you intent.

You are wanting instead to create a tree-view type of interface, which is quite different from what my above code is intended to solve.

You’re needing instead a vertical expandable menu, or a treeview solution instead.