Persisting Collapsible Table of Contents State

Web pages that employ a collapsible table of contents tend to share a single problem: when a user leaves the page, the table of contents reverts to its original state. For anyone trying to navigate through these nested references, it is frustrating to leave the page and return, only to have to wade through the table of contents again. Persistence behavior provides a solution to this problem.

In this example, the state of a collapsible table of contents will be preserved using the saveHistory behavior. The first step is to prepare a Web page to use the saveHistory behavior. Second, a table of contents with the saveHistory behavior will be created. Finally, a few lines of Microsoft JScript will be used to take advantage of the saveHistory behavior on the table of contents and persist the state when the user leaves the page.

Essential Persistence Preparations

The saveHistory behavior requires certain elements in order to function: a meta object, a STYLE block, and a CLASS attribute on the object to persist. An ID attribute is optional, though recommended.

The META Tag and STYLE Block

The meta and STYLE elements are used to inform the browser that the Web page is persistent.

<META NAME="save" CONTENT="history">
<STYLE>
   .saveHistory {behavior:url(#default#savehistory);}
</STYLE>

The CLASS Attribute

The CLASS attribute identifies the type of persistence the element is using.

<ELEMENT
  CLASS="saveHistory"
  ID="oPersistElement"
>

The STYLE Attribute Set Inline

The style can also be set inline with the element.

<ELEMENT
   CLASS="saveHistory"
   STYLE="behavior:url(#default#savehistory)"
   ID="oPersistElement"
>

Create a Table of Contents

In this example, the table of contents will be kept simple: a few nested unordered lists. The saveHistory behavior will be used with the getAttribute and setAttribute methods, and the onload and onsave events. The table of contents will be created first.

Several styles are included that will have a significant impact on this TOC. First, the saveHistory style. Second, tocItemHide and tocItemShow will be used to determine if a sublist is displayed or not.

Notice that the sublists use the class tocItemHide. The lists with this class will not be displayed. When the sublist needs to be displayed, the class is changed to tocItemShow.

//The META and STYLE tags are required
<META NAME="save" CONTENT="History">
//The STYLE is required
<STYLE>
   .saveHistory {behavior:url(#default#savehistory);}
   .tocItemHide {display: none;}
   .tocItemShow {display: block;}
</STYLE>
<!-- The tocItemHide and tocItemShow are used to set the display state. -->
:
<UL ID=oPersistTOC CLASS=saveHistory>
   <LI>Group 1
<!-- A class is given to the sub-lists,
     allowing them to collapse and expand. 
-->
   <UL CLASS="tocItemHide">
      <LI>Item 1
      <LI>Item 2
      <LI>Item 3
   </UL>
   <LI>Group 2
   <UL CLASS="tocItemHide">
      <LI>Item 1
      <LI>Item 2
      <LI>Item 3
   </UL>
   <LI>Group 3
   <UL CLASS="tocItemHide">
      <LI>Item 1
      <LI>Item 2
      <LI>Item 3
   </UL>
</UL>

Persist TOC State Using JScript

Persisting state can be hard to implement and maintain because the state of object styles will differ from page to page, and from user to user. It's important to invest a little time upfront to design a script and table of contents so that new items can be easily added or removed as the page is authored.

The script that will handle this TOC's persistence will take several things into account.

  1. The TOC can be modified without altering the script.
  2. The TOC is static and not dynamically generated. For example, this is not a TOC of today's news that may change from the time a user leaves the page to when the user returns.

Before writing the script, two event handlers should be set on the persistent ul object. This is the top-most ul object with a class equal to saveHistory. The onsave event handler will call the fnSave function to persist the TOC state. The onload event handler will call the fnLoad function, which will restore the persisted TOC state. The persistent object should now look like this:

<!-- An ID is used to refer to the table of contents.
     This is the only persistent object on the page.
     The onsave and onload event handlers are used
     to persist the state of the TOC.
-->
<UL
   ID="oPersistTOC"
   CLASS="saveHistory"
   onsave="fnSave()"
   onload="fnLoad()"
>
   <LI>Group 1
<!-- The state of the TOC is persisted by gathering
     the CLASS attribute values.
-->
   <UL CLASS="tocItemHide">
      <LI>Item 1
      <LI>Item 2
      <LI>Item 3
   </UL>
   :
</UL>

The last modification to the TOC is to enable the show and hide ability using the display style. A relatively easy way to do this is to include a div, a or span object with an onclick event handler that calls a function and passes this as an argument, referring to the object that was clicked. event.srcElement property could also be used to obtain a reference to the calling object. For this example, a div is used.

<UL ID="oPersistTOC"
   CLASS="saveHistory"
   onsave="fnSave()"
   onload="fnLoad()"
>
<!-- A DIV object with an onclick event handler is used to fire a function
     that will collapse and expand the list.
-->
   <LI><DIV onclick="fnFlash(this)" CLASS="tocHead">Group 1</DIV>
   <UL CLASS="tocItemHide">
      <LI>Item 1
      <LI>Item 2
      <LI>Item 3
   </UL>
   :
</UL>

Now, the fnSave, fnLoad and fnFlash functions need to be added. The fnFlash function is the easiest and is used to show and hide, or open and close, a TOC sublist.

On line 1, the function is defined and the this argument is passed as oTitle. The oList variable is defined on line 2 and is assigned a reference to the object right after the oTitle, which was defined by this. Finally, if oList has a className of tocItemHide, which would mean the sublist is hidden, then the class is changed to tocItemShow, which will reveal it. Otherwise, the class is changed to tocItemHide, which will hide the list.

function fnFlash(oTitle){
<!-- The collapsible UL object is the next object after the DIV
     that fired this function.  This is static, so any more objects
     between the DIV and UL would prevent this from working.
-->
   oList=document.all[oTitle.sourceIndex + 1];
   if(oList.className=="tocItemHide"){
      oList.className="tocItemShow";
   }
   else{
      oList.className="tocItemHide";
   }
}

The fnSave function is used to obtain all of the class names of the sublists in the persistent TOC, and then store them as an attribute on the persistent ul object. There are several different ways of storing this information, but this one will work with a TOC of any length and does not require a script statement or attribute for each line.

For multiple attribute values to be persisted, a delimited string array will be written to only one attribute. This keeps the number of persisting attributes at a manageable level. Since this string array will be automatically generated, saved, and loaded, it only needs to be defined as an empty string.

First, an empty variant is defined to store the string array for the class names. Second, the fnSave function looks through the document for all ul objects that have an object above assigned to the tocHead class. This is done to prevent inclusion of other ul objects within the document. Third, the sArray variant's first value is equal to the qualifying ul object's class name. After that, a new value will be added to sArray, prefixed with a delimiter. Finally, the persistent ul object with an ID of oPrimaryTOC is assigned an attribute called sPersistState that has a value of sArray, using the setAttribute method.

function fnSave(){
   var sArray = "";
/* Look at all of the UL objects that are preceded by an object
   containing a CLASS attribute equal to tocHead.
*/
   for(var i = 0;i < document.all.length;i++){
      oTrap = document.all[i];
      if((oTrap.tagName == "UL")
        &&(document.all[i-1].className == "tocHead")){
/* If one exists, and this is the first one found,
   add it to the string array.
*/
         if(sArray.length == 0){
            sArray = oTrap.className;
         }
/* If one exists but isn't the first,
   add it to a string array with a delimiter.
*/
         else sArray += "|" + oTrap.className;
      }
   }
/* Set an attribute on the persistent object
   with the string array as a value.
*/
   oPrimaryTOC.setAttribute("sPersistState",sArray);
}

To restore the TOC to the state it was in when the page was left, the fnLoad function is used. This function will be called from the onload event on the persistent ul object. Just as there are several different ways of restoring the information, it must be consistent with how the information was persisted. Thus, since the information was persisted within a string array as an attribute called sPersistState, these values must be restored and then set back onto the TOC.

The first step is to define a variant called sArray and give it the value of the sPersistState on the persistent ul object. This attribute is restored using the getAttribute method. An arbitrary counter called cycle is defined to walk through the ordinal values of sArray. A JScript array object, oArray, is defined and is populated by using the split method on the sArray variant. The delimiter that was added in the fnSave function is used as an argument. The first three lines of the fnLoad function have been devoted to restoring the persistent values and putting them into a manageable form.

The second step is to look throughout the web page again using the same criteria as that used in the fnSave function: objects with a class equal to tocHead that reside above ul objects. When these conditions are met, that particular ul object is given a value from the JScript array object. The array object's ordinal position is controlled by the arbitrary counter, and the counter's value is incremented.

function fnLoad(){
//Create a variable equal to the persistent information.
   var sArray = oPrimaryTOC.getAttribute("sPersistState");
   var cycle = 0;
//Chop up the string array and create an array object.
   oArray = sArray.split("|");
   for (var i = 0;i < document.all.length;i++){
/* Look in the document for any UL object preceded by an object
   containing a CLASS attribute with a value of tocHead.
*/
      oTrap = document.all[i];
      if((oTrap.tagName == "UL")
        &&(document.all[i-1].className == "tocHead")){
/* If one exists, assign that object a value from the array object,
   then increment a counter for the next.
*/
         oTrap.className = oArray[cycle];
         cycle++;
      }
   }
}

When the fnSave function is used to store the class names of tocItems, and the fnLoad function restores these values, the state of the TOC is persisted.

Code example: https://samples.msdn.microsoft.com/workshop/samples/author/persistence/saveHistory_ht1.htm