If you have worked on a SharePoint Online site that has more than 20-30 links in the top nav, you have likely noticed that the response time is slower than it feels like it should be. As the number of sites in the navigation increase so does the slowness until it become painful.
The culprit is structured navigation and its lack of efficiency.
There are a couple potential solutions to this. One being managed metadata navigation, the other being the topic of this post, Search Driven Navigation.
Put simply, search driven navigation uses a search query to create the navigation tree. This option requires a developer and this blog post is being written because the documentation out there was not sufficient for me to deliver an acceptable top nav situation to a client.
Support.office.com offers an incomplete solution that leaves out what js libraries you need to add, where to get them, and provides js that conflicts with at least some ribbon buttons, one being the explorer view button.
https://support.office.com/en-us/article/Navigation-options-for-SharePoint-Online-adb92b80-b342-4ecb-99a1-da2a2b4782eb
Raymond Little’s blog was extremely helpful and addressed most of the issues in the support.office.com post
https://raymondlittle.wordpress.com/2015/01/20/updates-needed-to-get-navigation-options-for-sharepoint-online-msdn-article-to-work/
I recommend giving both of these a read as they will be helpful in getting your head around exactly what we are doing here. That said this blog will serve as a step by step/start to finish solution.
The very first thing you want to do is add the following to your masterpage.
- You are going to need a custom master page for this, so if you don’t already have one make a copy of one of the OOTB master pages and add this to its html file…
- In my version the <![CDATA[ tags were added by designer, so they should be fine in yours, but if you run into any issues loading any of these scripts later on, you may want to remove them.
- Notice that you will need a custom css file for this solution to work. Obviously you are going to get errors from jump because you haven’t downloaded these libraries yet, but we will get there.
- You will need to either create a SearchNav folder in the Style Library and put the required files there or you will need to change the paths below to match where you put the files
- This all lives under this line in my masterpage. There is some weirdness with these showing up unless they are in the right place
- WordPress isn’t cool with script references, which makes sense, so the ones below look like links. Those will need to be script references.
- I highly recommend you use notepad++ or some other editor that lets you see what tags match etc when you are looking at the code in this post. It will make it much easier to sort out.
https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js
/Style%20Library/searchNav/linq.js
/Style%20Library/SearchNav/SearchTopNavigation.js
/Style%20Library/SearchNav/knockout-3.4.0.js
Next let’s download knockout and linq.js
For linq.js you’ll need to download the zip from here and pull the linq.js file from it – UPDATE. I’m adding the linq.js file that I use to this post because codeplex is going away and someone commented about an error in linq.js that they ran into, but I did not
Click here to download linq in a pdf
Now you have all the files that you need to download and you are ready to create the SearchTopNavigation.js file
Open your favorite editor , copy the following into it and put it in the appropriate folder. A few notes about this js
- I wrapped the bits under //Models and Namespaces in a IIFE to deal with the conflict with the ribbon buttons
- the addNav function is not needed unless your client wants to have a way to add additional links to a drop down on their top menu. The client I wrote this for wanted to be able to do that and basically had a quick links nav item that is tied to a list and allows them to add links via that list.
- isIEorEDGE() deals with the fact that IE and Edge deal with sorting the opposite way that FF and Chrome do. The rest of this is under //sorting stuff
- If you are using test accounts this is going to look weird to you as you switch between account because the hierarchy is saved to local storage and pulled from there, so as to make this more efficient. What that means for testing is that you’ll need to use different browsers or private browsing windows for different users or clear local storage often to see what that user will really see.
function checkLength(childCount)
{
if (childCount > 0)
{
return true;
}
else
{
return false;
}
}
function checkNoLength(childCount)
{
if (childCount == 0)
{
return true;
}
else
{
return false;
}
}
//only needed if you want to give client a way to add links
function addNav()
{
$.ajax({
url: “/_api/web/lists/getbytitle(‘added_Links’)/items?$orderby=NavOrder asc”,
type: “GET”,
headers: {“Accept”: “application/json;odata=verbose”},
cache:false,
success: function(data){
var items = [];
var count = 0;
var chCount;
$(data.d.results).each(function(){
items.push(‘
‘);
});
$(“#showNav”).html(items.join(”))}
});
}
function isIEorEDGE(){
if (navigator.appName == ‘Microsoft Internet Explorer’){
return true; // IE
}
else if(window.navigator.userAgent.indexOf(“Edge”) > -1){
// EDGE
return true;
} else if (!!navigator.userAgent.match(/Trident\/7\./)){
return true; }
return false;
}
//Models and Namespaces
(function () {var SPO = SPO || {};
SPO.Models = SPO.Models || {}
SPO.Models.NavigationNode = function () {
this.Url = ko.observable(“”);
this.Title = ko.observable(“”);
this.Parent = ko.observable(“”);
};
var isIE = isIEorEDGE();
var root = “https://yoursite.sharepoint.com”;
var baseUrl = root + “/_api/search/query?querytext=”;
var query = baseUrl + “‘contentClass=\”STS_Web\”-WebTemplate:APP+path:” + root + “‘&trimduplicates=false&rowlimit=300”;
var baseRequest = {
url: “”,
type: “”
};
//Parses a local object from JSON search result.
function getNavigationFromDto(dto) {
var item;
if (dto != undefined) {
item = new SPO.Models.NavigationNode();
item.Title(dto.Cells.results[3].Value);
item.Url(dto.Cells.results[6].Value);
item.Parent(dto.Cells.results[20].Value);
}
return item;
}
//Parse a local object from the serialized cache.
function getNavigationFromCache(dto) {
var item = new SPO.Models.NavigationNode();
if (dto != undefined) {
item.Title(dto.Title);
item.Url(dto.Url);
item.Parent(dto.Parent);
}
return item;
}
/* create a new OData request for JSON response */
function getRequest(endpoint) {
var request = baseRequest;
request.type = “GET”;
request.url = endpoint;
request.headers = { ACCEPT: “application/json;odata=verbose” };
return request;
};
/* Navigation Module*/
function NavigationViewModel() {
“use strict”;
var self = this;
self.nodes = ko.observableArray([]);
self.hierarchy = ko.observableArray([]);;
self.loadNavigatioNodes = function () {
//Check local storage for cached navigation datasource.
var fromStorage = localStorage[“nodesCache”];
if (fromStorage != null) {
var cachedNodes = JSON.parse(localStorage[“nodesCache”]);
var timeStamp = localStorage[“nodesCachedAt”];
if (cachedNodes && timeStamp) {
//Check for cache expiration. Currently set to 3 hrs.
var now = new Date();
var diff = now.getTime() – timeStamp;
if (Math.round(diff / (1000 * 60 * 60)) < 3) { //return from cache. var cacheResults = []; $.each(cachedNodes, function (i, item) { var nodeitem = getNavigationFromCache(item, true); cacheResults.push(nodeitem); }); var sortedArray = cacheResults.sort(self.sortObjectsInArray); self.buildHierarchy(sortedArray); self.toggleView(); return; } } } //No cache hit, REST call required. self.queryRemoteInterface(); }; //Executes a REST call and builds the navigation hierarchy. self.queryRemoteInterface = function () { var oDataRequest = getRequest(query); $.ajax(oDataRequest).done(function (data) { var results = []; $.each(data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results, function (i, item) { if (i == 0) { //Add root element. var rootItem = new SPO.Models.NavigationNode(); rootItem.Title(“Quick Links”); rootItem.Url(root); rootItem.Parent(null); results.push(rootItem); } var navItem = getNavigationFromDto(item); results.push(navItem); }); //Add to local cache localStorage[“nodesCache”] = ko.toJSON(results); localStorage[“nodesCachedAt”] = new Date().getTime(); self.nodes(results); if (self.nodes().length > 0) {
var unsortedArray = self.nodes();
var sortedArray = unsortedArray.sort(self.sortObjectsInArray);
self.buildHierarchy(sortedArray);
self.toggleView();
addEventsToElements();
}
}).fail(function () {
//Handle error here!!
$(“#loading”).hide();
$(“#error”).show();
});
};
self.toggleView = function () {
var navContainer = document.getElementById(“navContainer”);
ko.applyBindings(self, navContainer);
$(“#loading”).hide();
$(“#navContainer”).show();
};
//Uses linq.js to build the navigation tree.
self.buildHierarchy = function (enumerable) {
self.hierarchy(Enumerable.From(enumerable).ByHierarchy(function (d) {
return d.Parent() == null;
}, function (parent, child) {
if (parent.Url() == null || child.Parent() == null)
return false;
return parent.Url().toUpperCase() == child.Parent().toUpperCase();
}).ToArray());
};
self.sortObjectsInArray = function (a,b){
//sorting stuff
if(isIE){
if (a.Title() > b.Title())
return -1;
if (a.Title() < b.Title()) return 1; return 0; } else { if (a.Title() > b.Title())
return 1;
if (a.Title() < b.Title())
return -1;
return 0;
}
}
}
//Loads the navigation on load and binds the event handlers for mouse interaction.
$(document).ready(function () {
“use strict”;
_spBodyOnLoadFunctionNames.push(“addNav”);
addNav();
var viewModel = new NavigationViewModel();
viewModel.loadNavigatioNodes();
});
}());
Save this file to wherever your reference points in the master page and let’s move on to css. This is all my preference because frankly position this stuff via js was a nightmare and if someone used their zoom is caused more problems than it was worth.
My CSS looks like this…
#navContainer{
padding: 0;
margin: 0;
}
#navContainer ul {
float:left;
}
#navContainer a{
display:block;
text-decoration:none;
padding: 5px 5px 0px 5px;
color: rgb(102, 102, 102);
}
#navContainer a:hover{
color:rgb(72, 35, 93);
}
#navContainer li {
position:relative;
list-style: none;
}
#navContainer ul ul {
position: absolute;
left: 0px;
top:100%;
visibility:hidden;
width: 200px;
}
#navContainer ul ul ul {
left: 100%;
top: 0;
width: 200px;
}
#navContainer li:hover > ul {
visibility: visible;
}
save this to a css file and make sure to change your reference to point to it.
Finally, we are ready to replace the OOTB nav with our.
Do a find on DeltaTopNavigation and make it look like the following.
Some notes on this
- you may just want to leave the actual tag alone and replace the code in between it, starting with and ending with
- This code assumes you want flyouts, if you don’t adjust it accordingly by removing the second span that looks like this…
- You may not want the quick links. If you don’t just remove that section.
<!–SPM:–>
<!–SPM:–>
That should be it. I have set this up in a production environment and it works great. It is very speedy, and is free of conflicts. If you run into issues I recommend checking the designer tools to make sure that everything is loading properly etc.
If you have any questions or comments, please post them.