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