The ‘group by’ native functionality of XsltListViewWebPart is convenient to present a classified view on the contents of a SharePoint List / Library. Requirement of one of our departments is to go beyond that, and provide a tabbed view on the document library: a tab per month per year.
To achieve this, one basically has the following options:
- Build a custom webpart. However, this is so old-school SharePoint platform utilization; and in our company by default disallowed.
- Build a custom HTML / javascript UI (App), and connect via SharePoint webservices. Although this setup nicely fits in ‘modern SharePoint App-development’, for this specific scenario the drawback is that you then also need to develop yourself all of the Office integration that SharePoint standard delivers for a rendered document library (via ECB menu).
- Reuse XsltListViewWebPart, but specify an own Xslt-styling. This approach suffers from the same approach as the ‘modern App’ alternative: you’re then required to include in the custom Xstl all the code to render Office-integration friendly.
- Reuse XsltListViewWebPart, and dynamically modify the standard grouped-by layout into a tabbed view. Beyond reuse of the native Office-integration, this approach also reuses the lazy loading per group that is native in the XsltListViewWebPart group-by handling. Especially with a larger document library, this makes it much more performant as to retrieve the entire contents at once.
Client-side transfrom group-by layout into tabbed view
The transformation of the standard group-by layout into a tabbed view can be achieved as full client-side code only. To achieve the effect, I inspected the standard delivered html; and next coded the transformation logic in jQuery.Particulars
- The native 'group-by' functionality renders the header(s) of the groups. In a tabbed-view layout, the selected tab however already visualizes which group selected; and the group-headers are undesirable in the rendering.
- The native 'group-by' functionality opens new group in addition to the one(s) already open. For a tab-view experience, the views must be exclusive, act as toggles. Select one tab, automatically closes the tab selected before.
- The native 'group-by' functionality also includes a 'remember' function: by default a grouped-by layout opens with the group(s) opened as when the visitor was last on the page. For a consistent user-experience, it is then required to pre-select the associated tab-button.
The 'App' code
<style type="text/css">
.et-tab {
<ommitted…>
}
.et-tab-active {
<ommitted…>
}
.et-tab-inactive {
<ommitted…>
}
.et-separator {
height: 5px;
background-color: rgb(134, 206, 244);
}
</style>
<script>
var TabbedListView = window.TabbedListView || {};
TabbedListView.UI = function () {
function MonthToInt(month) {
<ommitted…>
}
function getCookieValue(cookieName) {
if (document.cookie.indexOf(cookieName) != -1) {
var cookies = document.cookie.split("; ");
for (var cookieSeq in cookies) {
var cookieSpec = cookies[cookieSeq];
if (cookieSpec.indexOf(cookieName) != -1
&& cookieSpec.indexOf("=") != -1
) {
return unescape(cookieSpec.split("=")[1]);
}
}
}
return undefined;
}
function TabbedView() {
var tabrow = $("<div class='et-tabrow'></div>");
$(".ms-listviewtable")
.before($(tabrow))
.before("<div class='et-separator'></div>");
$(".ms-listviewtable").children().each(function(i) {
// Grouping-row: level 0 or level 1
if ($(this).attr("groupString") !== undefined) {
// Month - lowest group level.
if ($(this).children("[id='group1']").length > 0) {
var action = $("<a></a>");
// Set the buttonlabel := '<month> <year>' by extracting
// the values from the original headings.
var monthValue =
$(this).find("a").parent().clone()
.children().remove().end().text().split(" : ")[1];
var parentId =
$(this).attr('id')
.substring(0, $(this).attr('id').length - 2);
var group0 =
$(this).parent().children("[id='" + parentId + "']");
var yearValue =
$(group0).find("a").parent().clone()
.children().remove().end().text().split(" : ")[1];
$(action).text(monthValue + " " + yearValue);
$(action).click(function() {
var parentId = $(this).parent().attr('id');
var parentTBodyId =
"titl" + parentId.substring(0, parentId.length -2;
var actualAA = $(".ms-listviewtable")
.find("tbody[id='" + parentTBodyId + "']").find("a");
if ($(actualAA).find('img').attr('src')
.endsWith("plus.gif")
) {
$(actualAA).trigger('click');
}
var actualA = $(".ms-listviewtable")
.find("tbody[id='titl" + parentId + "']").find("a");
$(actualA).trigger('click');
if ($(this).parent().hasClass("et-tab-inactive")) {
$(".ms-listviewtable").children().each(function(i) {
if ($(this).attr("groupString") !== undefined) {
$(this).hide();
}
});
$(".et-tabrow").children().each(function(i) {
if ($(this).hasClass("et-tab-active")) {
$(this).find("a").click();
}
});
$(this).parent().removeClass("et-tab-inactive");
$(this).parent().addClass("et-tab-active");
} else {
$(this).parent().removeClass("et-tab-active");
$(this).parent().addClass("et-tab-inactive");
}
});
// Add 'tab-button' to tab-row; in chronological sorted order.
var button = $("<span class='et-tab'></span>");
$(button).attr('id',
$(this).attr('id').substring(4, $(this).attr('id').length));
$(button).append($(action));
var totalMonths =
parseInt(yearValue) * 12 + MonthToInt(monthValue);
$(button).data('TotalMonths',totalMonths);
var added = false;
$(".et-tabrow").children().each(function(i) {
if (!added && parseInt($(this).data("TotalMonths")) > totalMonths) {
$(this).before($(button));
added = true;
}
});
if (!added) $(tabrow).append($(button));
$(button).addClass("et-tab-inactive");
}
$(this).hide();
}
});
ExecuteOrDelayUntilScriptLoaded(function() {
var cookieValue = getCookieValue("WSS_ExpGroup_");
var group1Opened = false;
if (cookieValue !== undefined) {
var expGroupParts = unescape(cookieValue).split(";#");
for (var i = 1; i < expGroupParts.length - 2; i++) {
if (expGroupParts[i+1] !== "&") {
group1Opened = true;
break;
} else {
i++;
}
}
}
if (group1Opened) {
// XsltListViewWebPart standard behaviour includes a 'remember'
// functionality: open the group(s) that was/were open before
// refreshing the page with the grouped-view. Overload that behaviour
// to make sure the 'tab-row' state is consistent with that.
$.prototype.base_ExpColGroupScripts = ExpColGroupScripts;
ExpColGroupScripts = function(c) {
var result = $.prototype.base_ExpColGroupScripts(c);
$(".ms-listviewtable").find("tbody[isLoaded]").each(function(i) {
if ($(this).find("td").text() === 'Loading....') {
var bodyId = $(this).attr('id')
.substring(4, $(this).attr('id').length-1);
var tabButton = $(".et-tabrow")
.children("[id='" + bodyId + "']");
if ($(tabButton).hasClass("et-tab-inactive")) {
$(tabButton).removeClass("et-tab-inactive");
$(tabButton).addClass("et-tab-active");
}
}
});
// Reset function
ExpColGroupScripts = $.prototype.base_ExpColGroupScripts;
return $(result);
};
} else {
$(".et-tabrow span:first-child").find("a").trigger('click');
}
}, "inplview.js");
$(".ms-listviewtable").show();
}
var ModuleInit = (function() {
$(".ms-listviewtable").hide();
_spBodyOnLoadFunctionNames.push("TabbedListView.UI.TabbedView");
})();
// Public interface
return {
TabbedView: TabbedView
}
}();
</script>
Update: support for multiple XsltListViewWebParts on page
The above 'App' code works fine in case of a single XsltListViewWebPart on page. However, in our company we also have document dashboards that give entrance to 'archived' and 'active' documents. The above code requires some update to be usable for 1 or more XsltListViewWebPart instances on a single page.
<style type="text/css">
<ommitted…>
</style>
<script>
var TabbedListView = window.TabbedListView || {};
TabbedListView.UI = function () {
function MonthToInt(month) {
<ommitted…>
}
function getCookieValue(cookieName) {
var cookieNameLC = cookieName.toLowerCase();
if (document.cookie.toLowerCase().indexOf(cookieNameLC) != -1) {
var cookies = document.cookie.split("; ");
for (var cookieSeq in cookies) {
var cookieSpec = cookies[cookieSeq];
if (cookieSpec.toLowerCase().indexOf(
cookieNameLC) != -1 && cookieSpec.indexOf("=") != -1) {
return unescape(cookieSpec.split("=")[1]);
}
}
}
return undefined;
}
var triggerCtxIsInit = false;
function initTabSelection(webpartId) {
var lstVw = $('div[WebPartID^="' + webpartId + '"]');
var cookieValue = getCookieValue("WSS_ExpGroup_{" + webpartId + "}");
var group1Opened = false;
if (cookieValue !== undefined) {
var expGroupParts = unescape(cookieValue).split(";#");
for (var i = 1; i < expGroupParts.length - 2; i++) {
if (expGroupParts[i+1] !== "&") {
group1Opened = true;
break;
} else {
i++;
}
}
}
if (group1Opened) {
// XsltListViewWebPart standard behaviour includes a 'remember'
// functionality: open the group(s) that was/were open before
// refreshing the page with the grouped-view. Overload that
// behaviour to make sure the 'tab-row' state is consistent with that.
if ($.prototype.base_ExpColGroupScripts === undefined) {
$.prototype.base_ExpColGroupScripts = ExpColGroupScripts;
ExpColGroupScripts = function(c) {
var result = $.prototype.base_ExpColGroupScripts(c);
$(".ms-listviewtable").find("tbody[isLoaded]").each(function(i) {
if ($(this).find("td").text() === 'Loading....') {
var bodyId = $(this).attr('id')
.substring(4, $(this).attr('id').length-1);
var tabButton =
$(".et-tabrow").children("[id='" + bodyId + "']");
if ($(tabButton).hasClass("et-tab-inactive")) {
$(tabButton).removeClass("et-tab-inactive");
$(tabButton).addClass("et-tab-active");
}
}
});
return $(result);
};
}
} else {
triggerCtxIsInit = true;
$(lstVw).parent().find(".et-tabrow span:first-child")
.find("a").trigger('click');
triggerCtxIsInit = false;
}
}
function TabbedView() {
$(".ms-listviewtable").each(function(i) {
ExecTabbedView($(this));
});
}
function ExecTabbedView(lstVw) {
var tabrow = $("<div class='et-tabrow'></div>");
$(lstVw).before($(tabrow)).before("<div class='et-separator'></div>");
$(lstVw).children().each(function(i) {
// Grouping-row: level 0 or level 1
if ($(this).attr("groupString") !== undefined) {
// Month - lowest group level.
if ($(this).children("[id='group1']").length > 0) {
var action = $("<a></a>");
// Set the buttonlabel := '<month> <year>' by extracting
// the values from the original headings.
var monthValue = $(this).find("a").parent().clone().children()
.remove().end().text().split(" : ")[1];
var parentId = $(this).attr('id')
.substring(0, $(this).attr('id').length - 2);
var group0 =
$(this).parent().children("[id='" + parentId + "']");
var yearValue = $(group0).find("a").parent().clone().children()
.remove().end().text().split(" : ")[1];
$(action).text(monthValue + " " + yearValue);
// Add clickhandler to:
// - check the 'parent-group-header in the table whether already
// opened; if not trigger it to open. This is required to reuse
// the standard XsltListViewWebPart behaviour wrt remember
// state upon refresh.
// - invoke the 'original' one of the group-header A in the
// table; to trigger the default behaviour
// - if 'selected':
// - hide the headings that are visualized by the default
// clickhandler
// - deselect the 'tab' that is current active
// - visualize the 'tab' to display as active
// - if 'deselected'
// - visualize the 'tab' to display as inactive
$(action).click(function() {
// On first user-initiated click; reset the overload of
// ExpColGroupScripts as only applicable on initialization.
if (!triggerCtxIsInit &&
$.prototype.base_ExpColGroupScripts !== undefined
) {
ExpColGroupScripts = $.prototype.base_ExpColGroupScripts;
$.prototype.base_ExpColGroupScripts = undefined;
}
var parentId = $(this).parent().attr('id');
var tabrow = $(this).parents('div[class^="et-tabrow"]');
var lstVw = $(tabrow).parent()
.find('table[class^="ms-listviewtable"]');
var actualAA = $(lstVw).find("tbody[id='titl" +
parentId.substring(0, parentId.length -2) + "']")
.find("a");
if ($(actualAA).find('img').attr('src')
.endsWith("plus.gif")
) {
$(actualAA).trigger('click');
}
var actualA = $(lstVw).find("tbody[id='titl" +
parentId + "']").find("a");
$(actualA).trigger('click');
if ($(this).parent().hasClass("et-tab-inactive")) {
$(lstVw).children().each(function(i) {
if ($(this).attr("groupString") !== undefined) {
$(this).hide();
}
});
$(tabrow).children().each(function(i) {
if ($(this).hasClass("et-tab-active")) {
$(this).find("a").click();
}
});
$(this).parent().removeClass("et-tab-inactive");
$(this).parent().addClass("et-tab-active");
} else {
$(this).parent().removeClass("et-tab-active");
$(this).parent().addClass("et-tab-inactive");
}
});
// Add 'tab-button' to tab-row; in chronological sorted order.
var button = $("<span class='et-tab'></span>");
$(button).attr('id', $(this).attr('id')
.substring(4, $(this).attr('id').length));
$(button).append($(action));
var totalMonths =
parseInt(yearValue) * 12 + MonthToInt(monthValue);
$(button).data('TotalMonths',totalMonths);
var added = false;
$(tabrow).children().each(function(i) {
if (!added && parseInt($(this).data("TotalMonths")) > totalMonths) {
$(this).before($(button));
added = true;
}
});
if (!added) $(tabrow).append($(button));
$(button).addClass("et-tab-inactive");
}
$(this).hide();
}
});
var webpartId = $(lstVw).parents('div[WebPartID^!=""]').attr('WebPartID');
ExecuteOrDelayUntilScriptLoaded(
function () { initTabSelection(webpartId) },
"inplview.js");
$(lstVw).show();
}
var ModuleInit = (function() {
$(".ms-listviewtable").each(function(i) {
$(this).hide();
});
_spBodyOnLoadFunctionNames.push("TabbedListView.UI.TabbedView");
})();
// Public interface
return {
TabbedView: TabbedView
}
}();
</script>