//============================================================================= | |
// System : Sandcastle Help File Builder | |
// File : TOC.js | |
// Author : Eric Woodruff (Eric@EWoodruff.us) | |
// Updated : 01/03/2011 | |
// Note : Copyright 2006-2011, 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://SHFB.CodePlex.com. 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); | |
var isChrome = (navigator.userAgent.indexOf("Chrome") >= 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); | |
// Chrome is too secure and won't let you access frame URLs when | |
// running from the file system unless you run Chrome with the | |
// "--disable-web-security" command line option. | |
if(isChrome && base.substr(0, 5) == "file:") | |
{ | |
alert("Chrome security prevents access to file-based frame " + | |
"URLs. As such, the TOC will not work with Index.html. " + | |
"Either run this website on a web server, run Chrome with " + | |
"the '--disable-web-security' command line option, or use " + | |
"FireFox or Internet Explorer."); | |
return ""; | |
} | |
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 = "<b>XML HTTP request not supported!</b>"; | |
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 - 1; | |
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 = "<b>XML HTTP request not supported!</b>"; | |
return; | |
} | |
divSearchResults.innerHTML = "<span class=\"PaddedText\">Searching...</span>"; | |
// 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]; | |
while(lastSearchNode != null && lastSearchNode.tagName != "A") | |
lastSearchNode = lastSearchNode.nextSibling; | |
if(lastSearchNode != null) | |
{ | |
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 = "<b>XML HTTP request not supported!</b>"; | |
return; | |
} | |
divIndexResults.innerHTML = "<span class=\"PaddedText\">Loading " + | |
"keyword index...</span>"; | |
// 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]; | |
while(lastIndexNode != null && lastIndexNode.tagName != "A") | |
lastIndexNode = lastIndexNode.nextSibling; | |
if(lastIndexNode != null) | |
{ | |
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; | |
} |