//=============================================================================================================== // System : Sandcastle Help File Builder // File : TOC.js // Author : Eric Woodruff (Eric@EWoodruff.us) // Updated : 03/26/2014 // Note : Copyright 2006-2014, 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: https://GitHub.com/EWSoftware/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. // 1.9.4.0 02/21/2012 EFW Merged code from Thomas Levesque to show direct link and support other page types // like PHP. // 1.9.5.0 07/25/2012 EFW Made changes to support IE 10. // 1.9.8.0 07/26/2013 EFW Merged changes from Dave Dansey to sync to TOC when the topic URL parameter is used //=============================================================================================================== // IE and Chrome flags var isIE = (navigator.userAgent.indexOf("MSIE") >= 0); var isIE10OrLater = /MSIE 1\d\./.test(navigator.userAgent); var isChrome = (navigator.userAgent.indexOf("Chrome") >= 0); // Page extension var pageExtension = ".aspx"; // 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; // XML Doc of the TOC var xmlTOCDoc //============================================================================ // Initialize the tree view and resize the content. Pass it the page extension to use (i.e. ".aspx") // for loading TOC element, index keywords, searching, etc. function Initialize(extension) { 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"); // Set the page extension if specified if(typeof(extension) != "undefined" && extension != "") pageExtension = extension; // The sizes are bit off in FireFox if(!isIE) divNavOpts.style.width = divSearchOpts.style.width = divIndexOpts.style.width = 292; ResizeTree(); SyncTOC(); topicContent.onload = 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) { // Don't allow references outside the current site if(options[idx + 1].length > 1 && options[idx + 1][0] != '/' && options[idx + 1][0] != '.') topicContent.src = options[idx + 1]; break; } } } //============================================================================ // Navigation and expand/collapse 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; } LoadTOC(url); 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; } // Search an array to see if it contains the given object function Contains(a, obj) { for(var i = 0; i < a.length; i++) if(a[i] === obj) return true; return false; } // Get the parent TOC IDs from the TOC XML file function GetParentTOCIds(target) { if(xmlTOCDoc == null) { // Load the TOC XML try { var xmlhttp = GetXmlHttpRequest(); xmlhttp.open("GET", "WebTOC.xml", false); xmlhttp.send(); xmlTOCDoc = xmlhttp.responseXML; } catch(e) { // alert(e.message); } if(xmlTOCDoc == null) return new Array(); } // Get all TOC nodes x = xmlTOCDoc.getElementsByTagName("HelpTOCNode"); // Iterate nodes looking for the target var targetNode = null; for(i = 0; i < x.length; i++) { var id = x[i].getAttribute('Url'); if(id != null) { id = id.substring(id.lastIndexOf("/") + 1, id.length - (id.length - id.lastIndexOf("."))); if(id == target) { targetNode = x[i]; break; } } } // Build an array of parent ids of the target node var ids = new Array(); if(targetNode != null) { var index = 0; while(targetNode.parentNode.tagName == "HelpTOCNode") { targetNode = targetNode.parentNode; ids[index] = targetNode.getAttribute('Id'); index = index + 1; } } return ids } // Load the TOC and expand all parent nodes down to the given entry function LoadTOC(url) { // Extract the target id from the url var target = url.substring(url.lastIndexOf("/") + 1, url.length - (url.length - url.lastIndexOf("."))); // Get an array of parent id's var idList = GetParentTOCIds(target); var divIdx, childIdx, img, divs = document.getElementsByTagName("DIV"); var childNodes, child, div; // Loop through all DIV tags, looking for the next one to lazy-load 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 == "Hidden" && Contains(idList,div.id)) { div.className = "Visible"; img.src = "Expanded.gif"; if(div.innerHTML == "") FillNodeAndTrySyncTOC(div) } } } // Lazy load the child TOC nodes and re-try to SyncTOC afterwards (if the TOC still can't be synced the process // will run again to expand the next parent down). function FillNodeAndTrySyncTOC(div) { 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" + pageExtension + "?Id=" + div.id + "&hash=" + now.getTime(), true); xmlHttp.onreadystatechange = function() { if(xmlHttp.readyState == 4) { div.innerHTML = xmlHttp.responseText; SyncTOC(); } } xmlHttp.send(null) } // 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 frame set that didn't make itself the topmost // frame set 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:///"); // Use lowercase on name for case-sensitive servers if(base.substr(0, 5) == "file:") top.location.href = base + "index.html"; else top.location.href = base + "index" + pageExtension; } 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" + pageExtension + "?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() { // IE 10 sizes the frame like FireFox and Chrome if(isIE && !isIE10OrLater) 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 = "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" + pageExtension + "?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 = "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" + pageExtension + "?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; } // Show a direct link to the currently displayed topic function ShowDirectLink() { var url = GetCurrentUrl(); var base = window.location.href; if(base.indexOf("?") > 0) base = base.substr(0, base.indexOf("?") + 1); base = base.substr(0, base.lastIndexOf("/") + 1); var relative = url.substr(base.length); // Using prompt lets the user copy it from the text box prompt("Direct link", base + "?topic=" + relative); }