//=============================================================================================================== // System : Sandcastle Help File Builder // File : branding-Website.js // Author : Eric Woodruff (Eric@EWoodruff.us) // Updated : 03/04/2015 // Note : Copyright 2014-2015, Eric Woodruff, All rights reserved // Portions Copyright 2014 Sam Harwell, All rights reserved // // This file contains the methods necessary to implement the lightweight TOC and search functionality. // // 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. // // Date Who Comments // ============================================================================================================== // 05/04/2014 EFW Created the code based on a combination of the lightweight TOC code from Sam Harwell and // the existing search code from SHFB. //=============================================================================================================== // Width of the TOC var tocWidth; // Search method (0 = To be determined, 1 = ASPX, 2 = PHP, anything else = client-side script var searchMethod = 0; // Table of contents script // Initialize the TOC by restoring its width from the cookie if present function InitializeToc() { tocWidth = parseInt(GetCookie("TocWidth", "280")); ResizeToc(); $(window).resize(SetNavHeight) } function SetNavHeight() { $leftNav = $("#leftNav") $topicContent = $("#TopicContent") leftNavPadding = $leftNav.outerHeight() - $leftNav.height() contentPadding = $topicContent.outerHeight() - $topicContent.height() // want outer height of left navigation div to match outer height of content leftNavHeight = $topicContent.outerHeight() - leftNavPadding $leftNav.css("min-height", leftNavHeight + "px") } // Increase the TOC width function OnIncreaseToc() { if(tocWidth < 1) tocWidth = 280; else tocWidth += 100; if(tocWidth > 680) tocWidth = 0; ResizeToc(); SetCookie("TocWidth", tocWidth); } // Reset the TOC to its default width function OnResetToc() { tocWidth = 0; ResizeToc(); SetCookie("TocWidth", tocWidth); } // Resize the TOC width function ResizeToc() { var toc = document.getElementById("leftNav"); if(toc) { // Set TOC width toc.style.width = tocWidth + "px"; var leftNavPadding = 10; document.getElementById("TopicContent").style.marginLeft = (tocWidth + leftNavPadding) + "px"; // Position images document.getElementById("TocResize").style.left = (tocWidth + leftNavPadding) + "px"; // Hide/show increase TOC width image document.getElementById("ResizeImageIncrease").style.display = (tocWidth >= 680) ? "none" : ""; // Hide/show reset TOC width image document.getElementById("ResizeImageReset").style.display = (tocWidth < 680) ? "none" : ""; } SetNavHeight() } // Toggle a TOC entry between its collapsed and expanded state function Toggle(item) { var isExpanded = $(item).hasClass("tocExpanded"); $(item).toggleClass("tocExpanded tocCollapsed"); if(isExpanded) { Collapse($(item).parent()); } else { var childrenLoaded = $(item).parent().attr("data-childrenloaded"); if(childrenLoaded) { Expand($(item).parent()); } else { var tocid = $(item).next().attr("tocid"); $.ajax({ url: "../toc/" + tocid + ".xml", async: true, dataType: "xml", success: function(data) { BuildChildren($(item).parent(), data); } }); } } } // HTML encode a value for use on the page function HtmlEncode(value) { // Create an in-memory div, set it's inner text (which jQuery automatically encodes) then grab the encoded // contents back out. The div never exists on the page. return $('
').text(value).html(); } // Build the child entries of a TOC entry function BuildChildren(tocDiv, data) { var childLevel = +tocDiv.attr("data-toclevel") + 1; var childTocLevel = childLevel >= 10 ? 10 : childLevel; var elements = data.getElementsByTagName("HelpTOCNode"); var isRoot = true; if(data.getElementsByTagName("HelpTOC").length == 0) { // The first node is the root node of this group, don't show it again isRoot = false; } for(var i = elements.length - 1; i > 0 || (isRoot && i == 0); i--) { var childHRef, childId = elements[i].getAttribute("Url"); if(childId != null && childId.length > 5) { // The Url attribute has the form "html/{childId}.htm" childHRef = childId.substring(5, childId.length); childId = childId.substring(5, childId.lastIndexOf(".")); } else { // The Id attribute is in raw form. There is no URL (empty container node). In this case, we'll // just ignore it and go nowhere. It's a rare case that isn't worth trying to get the first child. // Instead, we'll just expand the node (see below). childHRef = "#"; childId = elements[i].getAttribute("Id"); } var existingItem = null; tocDiv.nextAll().each(function() { if(!existingItem && $(this).children().last("a").attr("tocid") == childId) { existingItem = $(this); } }); if(existingItem != null) { // First move the children of the existing item var existingChildLevel = +existingItem.attr("data-toclevel"); var doneMoving = false; var inserter = tocDiv; existingItem.nextAll().each(function() { if(!doneMoving && +$(this).attr("data-toclevel") > existingChildLevel) { inserter.after($(this)); inserter = $(this); $(this).attr("data-toclevel", +$(this).attr("data-toclevel") + childLevel - existingChildLevel); if($(this).hasClass("current")) $(this).attr("class", "toclevel" + (+$(this).attr("data-toclevel") + " current")); else $(this).attr("class", "toclevel" + (+$(this).attr("data-toclevel"))); } else { doneMoving = true; } }); // Now move the existing item itself tocDiv.after(existingItem); existingItem.attr("data-toclevel", childLevel); existingItem.attr("class", "toclevel" + childLevel); } else { var hasChildren = elements[i].getAttribute("HasChildren"); var childTitle = HtmlEncode(elements[i].getAttribute("Title")); var expander = ""; if(hasChildren) expander = ""; var text = "
" + expander + "" + childTitle + "
"; tocDiv.after(text); } } tocDiv.attr("data-childrenloaded", true); } // Collapse a TOC entry function Collapse(tocDiv) { // Hide all the TOC elements after item, until we reach one with a data-toclevel less than or equal to the // current item's value. var tocLevel = +tocDiv.attr("data-toclevel"); var done = false; tocDiv.nextAll().each(function() { if(!done && +$(this).attr("data-toclevel") > tocLevel) { $(this).hide(); } else { done = true; } }); } // Expand a TOC entry function Expand(tocDiv) { // Show all the TOC elements after item, until we reach one with a data-toclevel less than or equal to the // current item's value var tocLevel = +tocDiv.attr("data-toclevel"); var done = false; tocDiv.nextAll().each(function() { if(done) { return; } var childTocLevel = +$(this).attr("data-toclevel"); if(childTocLevel == tocLevel + 1) { $(this).show(); if($(this).children("a").first().hasClass("tocExpanded")) { Expand($(this)); } } else if(childTocLevel > tocLevel + 1) { // Ignore this node, handled by recursive calls } else { done = true; } }); } // This is called to prepare for dragging the sizer div function OnMouseDown(event) { document.addEventListener("mousemove", OnMouseMove, true); document.addEventListener("mouseup", OnMouseUp, true); event.preventDefault(); } // Resize the TOC as the sizer is dragged function OnMouseMove(event) { tocWidth = (event.clientX > 700) ? 700 : (event.clientX < 100) ? 100 : event.clientX; ResizeToc(); } // Finish the drag operation when the mouse button is released function OnMouseUp(event) { document.removeEventListener("mousemove", OnMouseMove, true); document.removeEventListener("mouseup", OnMouseUp, true); SetCookie("TocWidth", tocWidth); } // Search functions // Transfer to the search page from a topic function TransferToSearchPage() { var searchText = document.getElementById("SearchTextBox").value.trim(); if(searchText.length != 0) document.location.replace(encodeURI("../search.html?SearchText=" + searchText)); } // Initiate a search when the search page loads function OnSearchPageLoad() { var queryString = decodeURI(document.location.search); if(queryString != "") { var idx, options = queryString.split(/[\?\=\&]/); for(idx = 0; idx < options.length; idx++) if(options[idx] == "SearchText" && idx + 1 < options.length) { document.getElementById("txtSearchText").value = options[idx + 1]; PerformSearch(); break; } } } // Perform a search using the best available method function PerformSearch() { var searchText = document.getElementById("txtSearchText").value; var sortByTitle = document.getElementById("chkSortByTitle").checked; var searchResults = document.getElementById("searchResults"); if(searchText.length == 0) { searchResults.innerHTML = "Nothing found"; return; } searchResults.innerHTML = "Searching..."; // Determine the search method if not done already. The ASPX and PHP searches are more efficient as they // run asynchronously server-side. If they can't be used, it defaults to the client-side script below which // will work but has to download the index files. For large help sites, this can be inefficient. if(searchMethod == 0) searchMethod = DetermineSearchMethod(); if(searchMethod == 1) { $.ajax({ type: "GET", url: encodeURI("SearchHelp.aspx?Keywords=" + searchText + "&SortByTitle=" + sortByTitle), success: function(html) { searchResults.innerHTML = html; } }); return; } if(searchMethod == 2) { $.ajax({ type: "GET", url: encodeURI("SearchHelp.php?Keywords=" + searchText + "&SortByTitle=" + sortByTitle), success: function(html) { searchResults.innerHTML = html; } }); return; } // Parse the keywords var keywords = ParseKeywords(searchText); // Get the list of files. We'll be getting multiple files so we need to do this synchronously. var fileList = []; $.ajax({ type: "GET", url: "fti/FTI_Files.json", dataType: "json", async: false, success: function(data) { $.each(data, function(key, val) { fileList[key] = val; }); } }); var letters = []; var wordDictionary = {}; var wordNotFound = false; // Load the keyword files for each keyword starting letter for(var idx = 0; idx < keywords.length && !wordNotFound; idx++) { var letter = keywords[idx].substring(0, 1); if($.inArray(letter, letters) == -1) { letters.push(letter); $.ajax({ type: "GET", url: "fti/FTI_" + letter.charCodeAt(0) + ".json", dataType: "json", async: false, success: function(data) { var wordCount = 0; $.each(data, function(key, val) { wordDictionary[key] = val; wordCount++; }); if(wordCount == 0) wordNotFound = true; } }); } } if(wordNotFound) searchResults.innerHTML = "Nothing found"; else searchResults.innerHTML = SearchForKeywords(keywords, fileList, wordDictionary, sortByTitle); } // Determine the search method by seeing if the ASPX or PHP search pages are present and working function DetermineSearchMethod() { var method = 3; try { $.ajax({ type: "GET", url: "SearchHelp.aspx", async: false, success: function(html) { if(html.substring(0, 8) == "") method = 1; } }); if(method == 3) $.ajax({ type: "GET", url: "SearchHelp.php", async: false, success: function(html) { if(html.substring(0, 8) == "") method = 2; } }); } catch(e) { } return method; } // Split the search text up into keywords function ParseKeywords(keywords) { var keywordList = []; var checkWord; var words = keywords.split(/\W+/); for(var idx = 0; idx < words.length; idx++) { checkWord = words[idx].toLowerCase(); if(checkWord.length > 2) { var charCode = checkWord.charCodeAt(0); if((charCode < 48 || charCode > 57) && $.inArray(checkWord, keywordList) == -1) keywordList.push(checkWord); } } return keywordList; } // Search for keywords and generate a block of HTML containing the results function SearchForKeywords(keywords, fileInfo, wordDictionary, sortByTitle) { var matches = [], matchingFileIndices = [], rankings = []; var isFirst = true; for(var idx = 0; idx < keywords.length; idx++) { var word = keywords[idx]; var occurrences = wordDictionary[word]; // All keywords must be found if(occurrences == null) return "Nothing found"; matches[word] = occurrences; var occurrenceIndices = []; // Get a list of the file indices for this match. These are 64-bit numbers but JavaScript only does // bit shifts on 32-bit values so we divide by 2^16 to get the same effect as ">> 16" and use floor() // to truncate the result. for(var ind in occurrences) occurrenceIndices.push(Math.floor(occurrences[ind] / Math.pow(2, 16))); if(isFirst) { isFirst = false; for(var matchInd in occurrenceIndices) matchingFileIndices.push(occurrenceIndices[matchInd]); } else { // After the first match, remove files that do not appear for all found keywords for(var checkIdx = 0; checkIdx < matchingFileIndices.length; checkIdx++) if($.inArray(matchingFileIndices[checkIdx], occurrenceIndices) == -1) { matchingFileIndices.splice(checkIdx, 1); checkIdx--; } } } if(matchingFileIndices.length == 0) return "Nothing found"; // Rank the files based on the number of times the words occurs for(var fileIdx = 0; fileIdx < matchingFileIndices.length; fileIdx++) { // Split out the title, filename, and word count var matchingIdx = matchingFileIndices[fileIdx]; var fileIndex = fileInfo[matchingIdx].split(/\0/); var title = fileIndex[0]; var filename = fileIndex[1]; var wordCount = parseInt(fileIndex[2]); var matchCount = 0; for(var idx = 0; idx < keywords.length; idx++) { occurrences = matches[keywords[idx]]; for(var ind in occurrences) { var entry = occurrences[ind]; // These are 64-bit numbers but JavaScript only does bit shifts on 32-bit values so we divide // by 2^16 to get the same effect as ">> 16" and use floor() to truncate the result. if(Math.floor(entry / Math.pow(2, 16)) == matchingIdx) matchCount += (entry & 0xFFFF); } } rankings.push({ Filename: filename, PageTitle: title, Rank: matchCount * 1000 / wordCount }); if(rankings.length > 99) break; } rankings.sort(function(x, y) { if(!sortByTitle) return y.Rank - x.Rank; return x.PageTitle.localeCompare(y.PageTitle); }); // Format and return the results var content = "
    "; for(var r in rankings) content += "
  1. " + rankings[r].PageTitle + "
  2. "; content += "
"; if(rankings.length < matchingFileIndices.length) content += "

Omitted " + (matchingFileIndices.length - rankings.length) + " more results

"; return content; }