Skip to main content

A neat little solution that uses Ajax to fetch definitions. Works with and without JavaScript available. Easily tailored to suit inclusion into a database.

-

An accessible AJAX glossary

author: mike foskett updated: 10th January 2008

The purpose of this project is to generically add a set of glossary terms to a web page. This should be achieved in an accessible and unobtrusive manner. The technique presented utlises AJAX to enhance user experience but does not have sole reliance upon them to display content.

A real world example demonstrating the glossary method may be found on the Next Generation Learning website.

A working example set of tests

The first word for testing, pedagogy, is a valid term.

The next term is a valid term but incorrectly marked-up by missing the class name Metadata instances. A class="glossary" is needed to fire the Ajax component but note that it still works by loading the glossary page then jumping to the term anchor.

The next example is a correctly coded non-existent term laoreet. Which should return a "term not found" message.

This test case is incorrectly coded (no class stated) and references a non-existent term Maecenas. It goes to the top of the full glossary page.

The next test demonstrates a poorly defined glossary term. It contains inapropriate block level elements, a ul list. beguile. Note in IE the HTML & AJAX version fails, but works in Firefox / Opera / Safari. In the PHP & AJAX version the term is parsed before delivery. Block elements are replaced with spans and thereby works as expected.

Finally further valid terms to prove multiple instances work: thesauri and taxonomies and just one more "lastChild" test vocabularies

How it works

Glossary links are distinguished by adding the class name class="glossary". Upon page load, JavaScript looks through each link ascertaining which are glossary links and attaches an alternate onclick action.

Without Javascript present the link simply goes to an anchor on a page displaying all the glossary terms.

With Javascript it fetches just the relevant term and places it in a pop-up box. Clicking the link a second time, or the pop-up, or another link removes the pop-up.

In the HTML the pop-up appears directly after the link thereby making it immediately available to screen readers et al.

On slow connections the pop-ups content is preceded by a "Please wait..." notification.

The link may be activated by keyboard and or mouse independently.

In the (X)HTML

Glossary definition coding

A glossary term must take the form:


<dt id="glossary_term">Glossary term</dt>
<dd>term definition one</dd>

A single term may have multiple definitions associated.

Each term is held in a separate html file eg glossary_term.html. This is to make data requests faster but it also simplifies the server-side inclusion into a database or CMS as required.

Note both the id and filename are the glossary term in lowercase with spaces replaced by underscores.

The HTML&AJAX version fails in IE if the term contains block level elements. While the PHP&AJAX version parses the definition replacing each with a span to prevent the issue.

Glossary link coding

A glossary link must take the form:


    <a class="glossary" href="glossary.php#glossary_term">Glossary term</a>
    

The JavaScript functions

onload handling


function addLoadEvent(func){
// author: Simon Willisons - http://simon.incutio.com/archive/2004/05/26/addLoadEvent
  if(!document.getElementById || !document.getElementsByTagName)return
  var oldonload=window.onload
  if(typeof window.onload!='function')window.onload=func
  else window.onload=function(){oldonload();func()}
}

Standard multiple onload function handler which allows multiple functions to be started upon fully loading the web page. Further information is available from the author Simon Willisons.

Check id exists, and replace content functions


function idExists(id){
  return (document.getElementById(id) ? true : false)
}

function replaceContent(id,content){
  if (idExists(id))
    document.getElementById(id).innerHTML = content
}

The CSS switching / checking / replacing function


function jsCSS(action,obj,class1,class2){
// author: Christian Heilmann - http://onlinetools.org
  switch (action){
    case 'swap':
        obj.className=!jsCSS('check',obj,class1)?obj.className.replace(class2,class1): obj.className.replace(class1,class2)
        break
    case 'add':
        if(!jsCSS('check',obj,class1)){obj.className+=obj.className?' '+class1:class1}
        break
    case 'remove':
        var rep=obj.className.match(' '+class1)?' '+class1:class1
        obj.className=obj.className.replace(rep,'')
        break
    case 'check':
        return new RegExp('\\b'+class1+'\\b').test(obj.className)
        break
  }
  return false
}

Further details from the author Christian Heilmann.

XHTTPRequest function


// XMLHttpRequest methods:
// author Jim Ley - http://jibbering.com/2002/4/httprequest.html
var xmlhttp=false
/*@cc_on @*/
/*@if (@_jscript_version>=5)
    try{xmlhttp=new ActiveXObject("Msxml2.XMLHTTP")}
    catch(e){
      try{xmlhttp=new ActiveXObject("Microsoft.XMLHTTP")}
      catch(E){xmlhttp=false}
    }
  @else
    xmlhttp=false
  @end @*/
if (!xmlhttp && typeof XMLHttpRequest!='undefined'){
  try{xmlhttp=new XMLHttpRequest()}
  catch(e){xmlhttp=false}
}
if (!xmlhttp && window.createRequest){
  try{xmlhttp=window.createRequest()}
  catch(e){xmlhttp=false}
}

Further details fropm the author Jim Ley.

Insert a HTML object after the current object


function insertAfter(targetElement, newElement){
  var parent = targetElement.parentNode
  if (parent.lastChild == targetElement)
    parent.appendChild(newElement)
  else
    parent.insertBefore(newElement, targetElement.nextSibling)
}

Find the screen position of current HTML object


function findPos(obj){
// author: Peter-Paul Koch - http://www.quirksmode.org/js/findpos.html
  var curleft=0, curtop=0
  if (obj.offsetParent){
    curleft = obj.offsetLeft
    curtop = obj.offsetTop
    while(obj = obj.offsetParent){
      curleft += obj.offsetLeft
      curtop += obj.offsetTop
  } }
  return [curleft, curtop]
}

Further details fropm the author Peter-Paul Koch.

Browser specific placement adjustments


function browserOffset(){
  var agt = navigator.userAgent.toLowerCase()
  if (agt.indexOf("msie") != -1) return 26
  if (agt.indexOf("firefox") != -1) return 26
  return 0
}

This function merely adjusts the display position slightly for certain browsers.

The main routine


function getAjaxObject(obj){
  // please wait notice
  obj.nextSibling.appendChild(waitImg)
  obj.nextSibling.appendChild(document.createTextNode(" Please wait..."))

  // get & set x y position
  obj.nextSibling.id = "term"
  document.getElementById('term').style.top = findPos(obj)[1] + browserOffset() + 'px'
  document.getElementById('term').style.left = findPos(obj)[0] + 'px'

  // this version calls glossary.php?term=glossary_term
  // parses and cleans the glossary term html via php
  var contentFile = obj.href.replace(/#/g, "?term=")
  if (xmlhttp){
    xmlhttp.open("GET", contentFile, true)
    xmlhttp.onreadystatechange = function(){
      if (xmlhttp.readyState == 4)
        replaceContent('term', xmlhttp.responseText)
    }
    xmlhttp.send(null)
  }
/*
  // this version fetches terms/glossary_term.html directly
  var contentFile = obj.href.replace(/glossary.php#/g, "terms/") + '.html'
  if (xmlhttp){
    xmlhttp.open("GET", contentFile, true)
    xmlhttp.onreadystatechange = function(){
      if (xmlhttp.readyState == 4)
        replaceContent('term', xmlhttp.responseText.replace(/<dt /, '<span class="dt" ').replace(/<\/dt>/, '</span>').replace(/<dd/g, '<span class="dd"').replace(/<\/dd>/g, '</span>'))
        // note: IE will not insert block code into an inline element
    }
    xmlhttp.send(null)
  }
*/
  return false
}

Reset the glossary link titles


function resetTitles(){
  for (var i = 0; i < glossaryLinks.length; i++)
    glossaryLinks[i].title = openText
}

Close all open glossary terms


function closeGlossaries(){
  for (var i = 0; i < glossaryLinks.length; i++){
    glossaryLinks[i].nextSibling.innerHTML = ""
    glossaryLinks[i].nextSibling.id = ""
} }

Achieved by removing content from all spans and by removing the id name.

A glossary link is clicked


function clickedTerm(obj){
  closeGlossaries()
  // If the term is closed it will have an "open me" title
  if (obj.title == openText){
    getAjaxObject(obj)
    resetTitles()
    obj.title = closeText
  }else
    resetTitles()
  obj.onclick = function(){
    clickedTerm(this)
    return false
  }
  obj.nextSibling.onclick = function(){
    closeGlossaries()
    resetTitles()
    return false
  }
  return false
}

Initialise the glossary


function setupGlossary(){
  // Get a list of all links in the content div
  var links = document.getElementById('content').getElementsByTagName('a')
  // work through each link
  for(var i = 0; i < links.length; i++){
    // if link's of glossary class
    if (jsCSS('check', links[i], 'glossary')){
      // add link to global array of glossay links
      glossaryLinks[glossaryLinks.length] = links[i]
      // insert a span (for the term) directly after the link
      insertAfter(links[i], document.createElement('span'))
      // change the links title attribute
      links[i].title = openText
      // add an onclick behaviour
      links[i].onclick = function(){
        clickedTerm(this)
        return false
} } } }

Initialise global variables


var waitImg = document.createElement('img')
waitImg.width = "32"
waitImg.height = "32"
waitImg.src = "please_wait.gif"
waitImg.alt = ""
var glossaryLinks = new Array()
var openText = "Expand glossary term"
var closeText = "Contract glossary term"

Initialise on page load


    addLoadEvent(setupGlossary)
    

CSS styling

In page glossary links


/* css for glossary link */
a.glossary,
a:link.glossary,
a:visited.glossary {
  text-decoration:none;
  border-bottom:1px dotted #090;
  padding-bottom:1px;
  font-weight:bold;
  cursor:help;
  color:green;
  position:relative;
  height:auto
  }
a:active.glossary,
a:focus.glossary,
a:hover.glossary {color:#0f0}

The glossary definition block


/* css for glossary term */
#term {
  position:absolute;
  left:-900em;
  top:0;
  width:20em;
  height:auto;
  z-index:100;
  font-size:smaller;
  font-weight:normal;
  line-height:120%;
  padding:0.25em 0.5em;
  margin:0;
  color:#040;
  background:url(close.gif) #fafffa no-repeat top right;
  border:1px solid green;
  cursor:help
  }
#term span {
  margin:0.5em 0 0.5em 1em;
  display:block
  }
#term span.dt {
  font-weight:bold;
  margin:0
  }

CSS for the back-up glossary page


/* css for glossary page */
dl {margin:0}
dt {
  font-weight:bold;
  margin:0.5em 0 0.25em 0
  }
dd {margin:0.25em 0 0.25em 1em}

The glossary of terms page

Ajax version first

On calling the page a php script runs and checks the url for $_GET data. If found it the page outputs the AJAX response of a single definition as held in the terms directory.

Otherwise the page outputs all glossary definitions and focuses on any URL passed anchor.


<?php

define('DATA_PATH','terms');

  if ($_GET['term']){
    if (file_exists(DATA_PATH)){
      $folder = opendir(DATA_PATH);
      $count = 0;
      $contents = '<dt id="' . $_GET['term'] . '">' . $_GET['term'] . '</dt><dd>Term not found in glossary</dd>';
      while($file = readdir($folder)){
        if ($file == $_GET['term'].'.html'){
          $filename = DATA_PATH.'/'.$file;
          $fd = fopen ($filename,'r') or die('Cannot open file '.$filename);
          $contents = fread ($fd,filesize($filename));
          fclose ($fd);
          break;
        }
        $count++;
      }
      closedir($folder);

Parse definition file to remove block level objects

The glossary_term.html file may contain html elements other than the expected <dt> and <dd>s. It is therefore parsed to replace any block level components with inline <span> elements. Besides being best practice it prevents IE throwing a wobbler when using innerHTML to insert illegal block elements inside an inline element.


// five conversions. Note each glossary term should only contain one dt and one or more dd's
// 1. convert expected open dt and dd's to span with class
      $contents = str_replace("<dt", ' <span class="dt"', $contents);
      $contents = str_replace("<dd", ' <span class="dd"', $contents);

// 3. convert open headings
      $headingElements = array("<h1", "<h3", "<h4", "<h4", "<h5", "<h6");
      $contents = str_replace($headingElements, '<span style="font-weight:bold"', $contents);

// 4. convert other open block elements
      $openBlockElements = array("<div", "<ul", "<ol", "<dl", "<li", "<p", "<blockquote", "<fieldset");
      $contents = str_replace($openBlockElements, "<span", $contents);

// 5. convert close block elements
      $closeBlockElements = array("</div", "</ul", "</ol", "</dl", "</dt", "</dd", "</li", "</p", "</h1", "</h3", "</h4", "</h4", "</h5", "</h6", "</blockquote", "</fieldset");
      $contents = str_replace($closeBlockElements, "</span", $contents);

      echo $contents;
    }
  }else{
?>

Followed by the normal HTML page version which is served if the page is called without a term=glossary_term attached to the url.

Read the "terms" directory and include all definitions

The content area has a simple routine which reads the filenames of all files in the DATAPATH (terms) directory. The data from each file is included into a definition list and output.


<?php
    if (file_exists(DATA_PATH)){
        $folder = opendir(DATA_PATH);
        $count = 0;
        echo('<dl>\n');
        while($file = readdir($folder)){
          if ($file[0] != "." && $file[0] != ".."){
            include(DATA_PATH . '/' . $file);
            $count++;
          }
        }
        closedir($folder);
        echo('</dl>\n');
    }
?>

Note in this simple version no sorting has been applied.

Downloadable files

Sorry, it appears I've accidentally deleted the download zip file associated with this project. I'll recreate them when time allows.

All I have available is the annotated JavaScript file.

Socialise: