| //============================================================================= |
| // 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; |
| } |