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.
