//=============================================================================
// System : Sandcastle Help File Builder
// File : TOC.js
// Author : Eric Woodruff (Eric@EWoodruff.us)
// Updated : 04/11/2008
// Note : Copyright 2006-2008, Eric Woodruff, All rights reserved
// Compiler: JavaScript
//
// This file contains the methods necessary to implement a simple tree view
// for the table of content with a resizable splitter and Ajax support to
// load tree nodes on demand. It also contains the script necessary to do
// full-text searches.
//
// This code is published under the Microsoft Public License (Ms-PL). A copy
// of the license should be distributed with the code. It can also be found
// at the project website: http://www.CodePlex.com/SHFB. This notice, the
// author's name, and all copyright notices must remain intact in all
// applications, documentation, and source files.
//
// Version Date Who Comments
// ============================================================================
// 1.3.0.0 09/12/2006 EFW Created the code
// 1.4.0.2 06/15/2007 EFW Reworked to get rid of frame set and to add
// support for Ajax to load tree nodes on demand.
// 1.5.0.0 06/24/2007 EFW Added full-text search capabilities
// 1.6.0.7 04/01/2008 EFW Merged changes from Ferdinand Prantl to add a
// website keyword index. Added support for "topic"
// query string option.
//=============================================================================
// IE flag
var isIE = (navigator.userAgent.indexOf("MSIE") >= 0);
// Minimum width of the TOC div
var minWidth = 100;
// Elements and sizing info
var divTOC, divSizer, topicContent, divNavOpts, divSearchOpts, divSearchResults,
divIndexOpts, divIndexResults, divTree, docBody, maxWidth, offset,
txtSearchText, chkSortByTitle;
// Last node selected
var lastNode, lastSearchNode, lastIndexNode;
// Last page with keyword index
var currentIndexPage = 0;
//============================================================================
// Initialize the tree view and resize the content
function Initialize()
{
docBody = document.getElementsByTagName("body")[0];
divTOC = document.getElementById("TOCDiv");
divSizer = document.getElementById("TOCSizer");
topicContent = document.getElementById("TopicContent");
divNavOpts = document.getElementById("divNavOpts");
divSearchOpts = document.getElementById("divSearchOpts");
divSearchResults = document.getElementById("divSearchResults");
divIndexOpts = document.getElementById("divIndexOpts");
divIndexResults = document.getElementById("divIndexResults");
divTree = document.getElementById("divTree");
txtSearchText = document.getElementById("txtSearchText");
chkSortByTitle = document.getElementById("chkSortByTitle");
// The sizes are bit off in FireFox
if(!isIE)
divNavOpts.style.width = divSearchOpts.style.width =
divIndexOpts.style.width = 292;
ResizeTree();
SyncTOC();
// Use an alternate default page if a topic is specified in
// the query string.
var queryString = document.location.search;
if(queryString != "")
{
var idx, options = queryString.split(/[\?\=\&]/);
for(idx = 0; idx < options.length; idx++)
if(options[idx] == "topic" && idx + 1 < options.length)
{
topicContent.src = options[idx + 1];
break;
}
}
}
//============================================================================
// Navigation and expand/collaps code
// Synchronize the table of content with the selected page if possible
function SyncTOC()
{
var idx, anchor, base, href, url, anchors, treeNode, saveNode;
base = window.location.href;
base = base.substr(0, base.lastIndexOf("/") + 1);
if(base.substr(0, 5) == "file:" && base.substr(0, 8) != "file:///")
base = base.replace("file://", "file:///");
url = GetCurrentUrl();
if(url == "")
return false;
if(url.substr(0, 5) == "file:" && url.substr(0, 8) != "file:///")
url = url.replace("file://", "file:///");
while(true)
{
anchors = divTree.getElementsByTagName("A");
anchor = null;
for(idx = 0; idx < anchors.length; idx++)
{
href = anchors[idx].href;
if(href.substring(0, 7) != 'http://' &&
href.substring(0, 8) != 'https://' &&
href.substring(0, 7) != 'file://')
href = base + href;
if(href == url)
{
anchor = anchors[idx];
break;
}
}
if(anchor == null)
{
// If it contains a "#", strip anything after that and try again
if(url.indexOf("#") != -1)
{
url = url.substr(0, url.indexOf("#"));
continue;
}
return;
}
break;
}
// If found, select it and find the parent tree node
SelectNode(anchor);
saveNode = anchor;
lastNode = null;
while(anchor != null)
{
if(anchor.className == "TreeNode")
{
treeNode = anchor;
break;
}
anchor = anchor.parentNode;
}
// Expand it and all of its parents
while(anchor != null)
{
Expand(anchor);
anchor = anchor.parentNode;
while(anchor != null)
{
if(anchor.className == "TreeNode")
break;
anchor = anchor.parentNode;
}
}
lastNode = saveNode;
// Scroll the node into view
var windowTop = lastNode.offsetTop - divTree.offsetTop - divTree.scrollTop;
var windowBottom = divTree.clientHeight - windowTop - lastNode.offsetHeight;
if(windowTop < 0)
divTree.scrollTop += windowTop - 30;
else
if(windowBottom < 0)
divTree.scrollTop -= windowBottom - 30;
}
// Get the currently loaded URL from the IFRAME
function GetCurrentUrl()
{
var base, url = "";
try
{
url = window.frames["TopicContent"].document.URL.replace(/\\/g, "/");
}
catch(e)
{
// If this happens the user probably navigated to another frameset
// that didn't make itself the topmost frameset and we don't have
// control of the other frame anymore. In that case, just reload
// our index page.
base = window.location.href;
base = base.substr(0, base.lastIndexOf("/") + 1);
if(base.substr(0, 5) == "file:" && base.substr(0, 8) != "file:///")
base = base.replace("file://", "file:///");
if(base.substr(0, 5) == "file:")
top.location.href = base + "Index.html";
else
top.location.href = base + "Index.aspx";
}
return url;
}
// Expand or collapse all nodes
function ExpandOrCollapseAll(expandNodes)
{
var divIdx, childIdx, img, divs = document.getElementsByTagName("DIV");
var childNodes, child, div, link, img;
for(divIdx = 0; divIdx < divs.length; divIdx++)
if(divs[divIdx].className == "Hidden" ||
divs[divIdx].className == "Visible")
{
childNodes = divs[divIdx].parentNode.childNodes;
for(childIdx = 0; childIdx < childNodes.length; childIdx++)
{
child = childNodes[childIdx];
if(child.className == "TreeNodeImg")
img = child;
if(child.className == "Hidden" || child.className == "Visible")
{
div = child;
break;
}
}
if(div.className == "Visible" && !expandNodes)
{
div.className = "Hidden";
img.src = "Collapsed.gif";
}
else
if(div.className == "Hidden" && expandNodes)
{
div.className = "Visible";
img.src = "Expanded.gif";
if(div.innerHTML == "")
FillNode(div, true)
}
}
}
// Toggle the state of the specified node
function Toggle(node)
{
var i, childNodes, child, div, link;
childNodes = node.parentNode.childNodes;
for(i = 0; i < childNodes.length; i++)
{
child = childNodes[i];
if(child.className == "Hidden" || child.className == "Visible")
{
div = child;
break;
}
}
if(div.className == "Visible")
{
div.className = "Hidden";
node.src = "Collapsed.gif";
}
else
{
div.className = "Visible";
node.src = "Expanded.gif";
if(div.innerHTML == "")
FillNode(div, false)
}
}
// Expand the selected node
function Expand(node)
{
var i, childNodes, child, div, img;
// If not valid, don't bother
if(GetCurrentUrl() == "")
return false;
if(node.tagName == "A")
childNodes = node.parentNode.childNodes;
else
childNodes = node.childNodes;
for(i = 0; i < childNodes.length; i++)
{
child = childNodes[i];
if(child.className == "TreeNodeImg")
img = child;
if(child.className == "Hidden" || child.className == "Visible")
{
div = child;
break;
}
}
if(lastNode != null)
lastNode.className = "UnselectedNode";
div.className = "Visible";
img.src = "Expanded.gif";
if(node.tagName == "A")
{
node.className = "SelectedNode";
lastNode = node;
}
if(div.innerHTML == "")
FillNode(div, false)
return true;
}
// Set the style of the specified node to "selected"
function SelectNode(node)
{
// If not valid, don't bother
if(GetCurrentUrl() == "")
return false;
if(lastNode != null)
lastNode.className = "UnselectedNode";
node.className = "SelectedNode";
lastNode = node;
return true;
}
//============================================================================
// Ajax-related code used to fill the tree nodes on demand
function GetXmlHttpRequest()
{
var xmlHttp = null;
// If IE7, Mozilla, Safari, etc., use the native object.
// Otherwise, use the ActiveX control for IE5.x and IE6.
if(window.XMLHttpRequest)
xmlHttp = new XMLHttpRequest();
else
if(window.ActiveXObject)
xmlHttp = new ActiveXObject("MSXML2.XMLHTTP.3.0");
return xmlHttp;
}
// Perform an AJAX-style request for the contents of a node and put the
// contents into the empty div.
function FillNode(div, expandChildren)
{
var xmlHttp = GetXmlHttpRequest(), now = new Date();
if(xmlHttp == null)
{
div.innerHTML = "XML HTTP request not supported!";
return;
}
div.innerHTML = "Loading...";
// Add a unique hash to ensure it doesn't use cached results
xmlHttp.open("GET", "FillNode.aspx?Id=" + div.id + "&hash=" +
now.getTime(), true);
xmlHttp.onreadystatechange = function()
{
if(xmlHttp.readyState == 4)
{
div.innerHTML = xmlHttp.responseText;
if(expandChildren)
ExpandOrCollapseAll(true);
}
}
xmlHttp.send(null)
}
//============================================================================
// Resizing code
// Resize the tree div so that it fills the document body
function ResizeTree()
{
var y, newHeight;
if(self.innerHeight) // All but IE
y = self.innerHeight;
else // IE - Strict
if(document.documentElement && document.documentElement.clientHeight)
y = document.documentElement.clientHeight;
else // Everything else
if(document.body)
y = document.body.clientHeight;
newHeight = y - parseInt(divNavOpts.style.height, 10) - 6;
if(newHeight < 50)
newHeight = 50;
divTree.style.height = newHeight;
newHeight = y - parseInt(divSearchOpts.style.height, 10) - 6;
if(newHeight < 100)
newHeight = 100;
divSearchResults.style.height = newHeight;
newHeight = y - parseInt(divIndexOpts.style.height, 10) - 6;
if(newHeight < 25)
newHeight = 25;
divIndexResults.style.height = newHeight;
// Resize the content div
ResizeContent();
}
// Resize the content div
function ResizeContent()
{
if(isIE)
maxWidth = docBody.clientWidth;
else
maxWidth = docBody.clientWidth - 4;
topicContent.style.width = maxWidth - (divSizer.offsetLeft +
divSizer.offsetWidth);
maxWidth -= minWidth;
}
// This is called to prepare for dragging the sizer div
function OnMouseDown(event)
{
var x;
// Make sure the splitter is at the top of the z-index
divSizer.style.zIndex = 5000;
// The content is in an IFRAME which steals mouse events so
// hide it while resizing.
topicContent.style.display = "none";
if(isIE)
x = window.event.clientX + document.documentElement.scrollLeft +
document.body.scrollLeft;
else
x = event.clientX + window.scrollX;
// Save starting offset
offset = parseInt(divSizer.style.left, 10);
if(isNaN(offset))
offset = 0;
offset -= x;
if(isIE)
{
document.attachEvent("onmousemove", OnMouseMove);
document.attachEvent("onmouseup", OnMouseUp);
window.event.cancelBubble = true;
window.event.returnValue = false;
}
else
{
document.addEventListener("mousemove", OnMouseMove, true);
document.addEventListener("mouseup", OnMouseUp, true);
event.preventDefault();
}
}
// Resize the TOC and content divs as the sizer is dragged
function OnMouseMove(event)
{
var x, pos;
// Get cursor position with respect to the page
if(isIE)
x = window.event.clientX + document.documentElement.scrollLeft +
document.body.scrollLeft;
else
x = event.clientX + window.scrollX;
left = offset + x;
// Adjusts the width of the TOC divs
pos = (event.clientX > maxWidth) ? maxWidth :
(event.clientX < minWidth) ? minWidth : event.clientX;
divTOC.style.width = divSearchResults.style.width =
divIndexResults.style.width = divTree.style.width = pos;
if(!isIE)
pos -= 8;
divNavOpts.style.width = divSearchOpts.style.width =
divIndexOpts.style.width = pos;
// Resize the content div to fit in the remaining space
ResizeContent();
}
// Finish the drag operation when the mouse button is released
function OnMouseUp(event)
{
if(isIE)
{
document.detachEvent("onmousemove", OnMouseMove);
document.detachEvent("onmouseup", OnMouseUp);
}
else
{
document.removeEventListener("mousemove", OnMouseMove, true);
document.removeEventListener("mouseup", OnMouseUp, true);
}
// Show the content div again
topicContent.style.display = "inline";
}
//============================================================================
// Search code
function ShowHideSearch(show)
{
if(show)
{
divNavOpts.style.display = divTree.style.display = "none";
divSearchOpts.style.display = divSearchResults.style.display = "";
}
else
{
divSearchOpts.style.display = divSearchResults.style.display = "none";
divNavOpts.style.display = divTree.style.display = "";
}
}
// When enter is hit in the search text box, do the search
function OnSearchTextKeyPress(evt)
{
if(evt.keyCode == 13)
{
PerformSearch();
return false;
}
return true;
}
// Perform a keyword search
function PerformSearch()
{
var xmlHttp = GetXmlHttpRequest(), now = new Date();
if(xmlHttp == null)
{
divSearchResults.innerHTML = "XML HTTP request not supported!";
return;
}
divSearchResults.innerHTML = "Searching...";
// Add a unique hash to ensure it doesn't use cached results
xmlHttp.open("GET", "SearchHelp.aspx?Keywords=" + txtSearchText.value +
"&SortByTitle=" + (chkSortByTitle.checked ? "true" : "false") +
"&hash=" + now.getTime(), true);
xmlHttp.onreadystatechange = function()
{
if(xmlHttp.readyState == 4)
{
divSearchResults.innerHTML = xmlHttp.responseText;
lastSearchNode = divSearchResults.childNodes[0].childNodes[1];
if(lastSearchNode != null)
{
if(lastSearchNode.tagName != "A")
lastSearchNode = lastSearchNode.nextSibling;
SelectSearchNode(lastSearchNode);
topicContent.src = lastSearchNode.href;
}
}
}
xmlHttp.send(null)
}
// Set the style of the specified search result node to "selected"
function SelectSearchNode(node)
{
if(lastSearchNode != null)
lastSearchNode.className = "UnselectedNode";
node.className = "SelectedNode";
lastSearchNode = node;
return true;
}
//============================================================================
// KeyWordIndex code
function ShowHideIndex(show)
{
if(show)
{
PopulateIndex(currentIndexPage);
divNavOpts.style.display = divTree.style.display = "none";
divIndexOpts.style.display = divIndexResults.style.display = "";
}
else
{
divIndexOpts.style.display = divIndexResults.style.display = "none";
divNavOpts.style.display = divTree.style.display = "";
}
}
// Populate keyword index
function PopulateIndex(startIndex)
{
var xmlHttp = GetXmlHttpRequest(), now = new Date();
var firstNode;
if(xmlHttp == null)
{
divIndexResults.innerHTML = "XML HTTP request not supported!";
return;
}
divIndexResults.innerHTML = "Loading " +
"keyword index...";
// Add a unique hash to ensure it doesn't use cached results
xmlHttp.open("GET", "LoadIndexKeywords.aspx?StartIndex=" + startIndex +
"&hash=" + now.getTime(), true);
xmlHttp.onreadystatechange = function()
{
if(xmlHttp.readyState == 4)
{
divIndexResults.innerHTML = xmlHttp.responseText;
if(startIndex > 0)
{
firstNode = divIndexResults.childNodes[1];
if(firstNode != null && !firstNode.innerHTML)
firstNode = divIndexResults.childNodes[2];
}
else
firstNode = divIndexResults.childNodes[0];
if(firstNode != null)
lastIndexNode = firstNode.childNodes[0];
if(lastIndexNode != null)
{
if(lastIndexNode.tagName != "A")
lastIndexNode = lastIndexNode.nextSibling;
SelectIndexNode(lastIndexNode);
topicContent.src = lastIndexNode.href;
}
currentIndexPage = startIndex;
}
}
xmlHttp.send(null)
}
// Set the style of the specified keyword index node to "selected"
function SelectIndexNode(node)
{
if(lastIndexNode != null)
lastIndexNode.className = "UnselectedNode";
node.className = "SelectedNode";
lastIndexNode = node;
return true;
}
// Changes the current page with keyword index forward or backward
function ChangeIndexPage(direction)
{
PopulateIndex(currentIndexPage + direction);
return false;
}