| //=============================================================================================================== |
| // 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 $('<div/>').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 = "<a class=\"tocCollapsed\" onclick=\"javascript: Toggle(this);\" href=\"#!\"></a>"; |
| |
| var text = "<div class=\"toclevel" + childTocLevel + "\" data-toclevel=\"" + childLevel + "\">" + |
| expander + "<a data-tochassubtree=\"" + hasChildren + "\" href=\"" + childHRef + "\" title=\"" + |
| childTitle + "\" tocid=\"" + childId + "\"" + |
| (childHRef == "#" ? " onclick=\"javascript: Toggle(this.previousSibling);\"" : "") + ">" + |
| childTitle + "</a></div>"; |
| |
| 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 = "<strong>Nothing found</strong>"; |
| 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 = "<strong>Nothing found</strong>"; |
| 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) == "<strong>") |
| method = 1; |
| } |
| }); |
| |
| if(method == 3) |
| $.ajax({ |
| type: "GET", |
| url: "SearchHelp.php", |
| async: false, |
| success: function(html) |
| { |
| if(html.substring(0, 8) == "<strong>") |
| 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 "<strong>Nothing found</strong>"; |
| |
| 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 "<strong>Nothing found</strong>"; |
| |
| // 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 = "<ol>"; |
| |
| for(var r in rankings) |
| content += "<li><a href=\"" + rankings[r].Filename + "\" target=\"_blank\">" + |
| rankings[r].PageTitle + "</a></li>"; |
| |
| content += "</ol>"; |
| |
| if(rankings.length < matchingFileIndices.length) |
| content += "<p>Omitted " + (matchingFileIndices.length - rankings.length) + " more results</p>"; |
| |
| return content; |
| } |