jQuery is a powerful tool, and so I always use it in my APEX projects to help accomplish what I wouldn't be able to with just apex/html/css. One of the things I almost always include is the Tablesorter, but there are a few issues when trying to make it work with Salesfroce standard page markup tags. All the documentation for the tablesorter is on their page: Tablesorter, but some points to remember are that if you are using any apex:output(filed/text) that it wraps a span around the text and the tablesorter needs to know that, also if you use an 'a' tag it behaves differently as well. Also there seems to be an issue with using the default sorter params, ie letting the tablesorter pick them for you. To show you a good example of the jQuery table sorter in action with salesforce, I will be using the 'User Info List' that I recently just updated with it.
(NOTE: that table sorter requires the class tablesorter on the table tag in order to work)
Example Table markeup:
<td><apex:outputfield value="{!c.Email}" /></td>
<td>{!c.Name}</td>
<td><span><apex:outputfield value="{!c.Phone}" /></span></td>
<td><a target="_blank" href="/{!c.Id}" title="go to Contact">Contact</a></td>
JS markup:
//this checks the table for data on three levels
//<td>the data</td> $(node).text();
//<td><node>the data</node></td> $(node).next().text();
//<td><node><node>the data</node></node></td> $(node).next().next().text();
//this ensures that the data is found no matter how many nodes it needs to transverse.
var myTextExtraction = function(node)
{
var nodeText = $(node).text();
if(nodeText == null || nodeText == ''){
nodeText = $(node).next().text();
}
if(nodeText == null || nodeText == ''){
nodeText = $(node).next().next().text();
}
return nodeText;
}
//example markup to use the node traversing and establish the sorter params
textExtraction: myTextExtraction,
headers: {
0: {sorter: 'text'},
1: {sorter: 'text'},
2: {sorter: 'text'},
3: {sorter: 'text'},
4: {sorter: 'shortDate'}
}
});
Now lets look at the User-Info-List page, you'll notice that instead of using the standard apex:pageblocktable I am using a standard table, BUT I have included the style(classes/structure) of what the apex:pageblocktable would have... meaning that if you right click and view source on an apex:pageblocktable you will see the same as below (style/class wise minus a few things for my own customizations) and so when this page renders, it looks like an apex:pageblocktable. I do it this way due to the lack of flexibility in the standard apex:pageblocktable, and the blocktable seems to clash with the jQuery tablesorter when used naively.
<apex:page controller="UserInfoList" standardStylesheets="false" title="User Info" action="{!SwitchList}">
<apex:stylesheet value="{!URLFOR($Resource.UserList, '/NewTQC.css')}"/>
<apex:stylesheet value="{!URLFOR($Resource.UserList, '/style.css')}"/>
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.UserList, '/js/jquery1_4.js')}" ></script>
<!-- always include the tablesorter script after the jQuery -->
<script type="text/javascript" language="javascript" src="{!URLFOR($Resource.UserList, '/js/jquery.tablesorter.min.js')}" ></script>
<apex:form >
<div id="pageWrap">
<!--wrap the whole page in an outputpanel so that ajax rerenders, it triggers the jQuery.ready() function-->
<apex:outputPanel id="TicketListWrap" styleClass="Wrap">
<script language="javascript" type="text/javascript">
//controls the loading gif
function Loading(b){
if(b){
$('.loadingIcon').removeClass('hideIt');
}else{
$('.loadingIcon').addClass('hideIt');
}
}
$(document).ready(function(){
//important to specify the sorting param otherwise it wont always choose the 'correct' one
$("table").tablesorter({
headers: {
0: {sorter: 'text'},
1: {sorter: 'text'},
2: {sorter: 'digit'},
3: {sorter: 'digit'},
4: {sorter: 'text'},
5: {sorter: 'text'},
6: {sorter: 'text'}
}
});
});
</script>
<div class="clear"></div>
<br />
<div id="newTicketHeader" class="floatL">Users in {!sDepartment}</div>
<div class="floatR">
<strong>Department:</strong>
<apex:selectList id="department" size="1" value="{!sDepartment}" >
<apex:selectOptions value="{!lstDepartmentOptions}" />
<!-- every time the select list is changed it refreshes the list of users -->
<apex:actionSupport event="onchange" onsubmit="Loading(true);" action="{!SwitchList}" rerender="TicketListWrap"id="actionsupportforKnownIssues" oncomplete="Loading(false);" />
</apex:selectList>
</div>
<!-- the loading icon -->
<div class="loadingIcon hideIt"><apex:image id="loadingImage" value="{!URLFOR($Resource.UserList, 'images/loader_24x26.gif')}"width="24" height="26"/></div>
<div class="clear"></div>
<div style="margin-left:5px; font-size:10px;">User count: {!iUserCount}</div>
<table id="Usertable" class="list tablesorter" cellspacing="1">
<thead class="rich-table-thead">
<tr class="headerRow">
<th colspan="1" scope="col">Name</th>
<th colspan="1" scope="col">Phone</th>
<th colspan="1" scope="col" style="width:50px;">Ext</th>
<th colspan="1" scope="col">ICQ</th>
<th colspan="1" scope="col">Title</th>
<th colspan="1" scope="col">Location</th>
<th colspan="1" scope="col">Action</th>
</tr>
</thead>
<tbody>
<apex:repeat value="{!CurrentListofUsers}" var="u" id="UserListRepeater">
<tr class="dataRow">
<td>{!u.name}</td>
<td>{!u.phone}</td>
<td>{!u.Extension}</td>
<td>{!u.ICQ__c}</td>
<td>{!u.Title}</td>
<td>{!u.Location__c}</td>
<td>
<div>
<!-- only current user can edit their own info -->
<apex:outputlink value="/{!u.id}/e?retURL=apex/UserInfoList" rendered="{!IF(u.id == $User.Id, true, false)}" >Edit Info</apex:outputlink>
</div>
<!-- only manager's/Team lead's/VP's can edit the users Queue info -->
<apex:outputpanel rendered="{!IF(CONTAINS($UserRole.Name, 'Manager') || CONTAINS($UserRole.Name, 'VP') || CONTAINS($UserRole.Name, 'Team Lead'), true, false)}">
<apex:outputlink value="/apex/QueueMemberEdit?UserId={!u.id}" >Edit Queues</apex:outputlink>
</apex:outputpanel>
</td>
</tr>
</apex:repeat>
</tbody></table>
<hr />
</apex:outputPanel>
</div>
</apex:form>
</apex:page>
The classes that power it:
public with sharing class UserInfoList {
public string sDepartment{get;set;}
public string sCurrentUserDepartment
{
get{return UserInfoListHelper.FindDepartment(UserInfo.getUserId());}
set;
}
public List<User> CurrentListofUsers
{
get{return UserInfoListHelper.UserList( UserInfoListHelper.FindDepartment(sCurrentUserDepartment, sDepartment));}
set;
}
public integer iUserCount
{
get{return CurrentListofUsers.size();}
set;
}
public List<selectOption> lstDepartmentOptions
{
get{return UserInfoListHelper.DepartmentKeys();}
set{lstDepartmentOptions = value;}
}
public pageReference SwitchList()
{
if(!GlobalHelper.CheckForNotNull(sDepartment))
{
sDepartment = sCurrentUserDepartment;
}
return null;
}
}
public with sharing class UserInfoListHelper {
/// <summary>
/// OVERLOADED
/// FINDS THE USERS DEPARTMENT BASED ON ID PASSED
/// </summary>
/// <param name="lstInValueToCheck">USER ID</param>
/// <returns>USER DEPARTMENT</returns>
public static string FindDepartment(string sInUserId)
{
if(GlobalHelper.CheckForNotNull(sInUserId))
{
try
{
User u = [select id, Department from User where id=:sInUserId limit 1];
return u.Department;
}
catch(exception e)
{
system.debug('UserInfoListHelper - FindDepartment - error: '+e);
}
}
return null;
}
/// <summary>
/// OVERLOADED
/// RETURNS USER DEPARTMENT IF SELECTED DEPARMENT IS NULL
/// </summary>
/// <param name="sInDepartmentSelection">USER DEPARMENT</param>
/// <param name="sInUserDepartment">SELECTED DEPARTMENT</param>
/// <returns>DEPARTMENT</returns>
public static string FindDepartment(string sInUserDepartment, string sInDepartmentSelection)
{
if(GlobalHelper.CheckForNotNull(sInDepartmentSelection))
{
return sInDepartmentSelection;
}
return sInUserDepartment;
}
/// <summary>
/// ADDS DEPARTMENT KEY NAMES TO LIST OF SELECT OPTIONS
/// </summary>
/// <param name="lstInUser">USER ID</param>
/// <returns>DEPARTMENT SELECT OPTION LIST</returns>
public static List<selectOption> AddItemToList(List<User> lstInUser)
{
List<SelectOption> lstOptions = new List<SelectOption>();
if(GlobalHelper.CheckForNotNull(lstInUser))
{
for(User u : lstInUser)
{
if(!GlobalHelper.ContainsItem(u.Department, lstOptions))
{
lstOptions.add(new SelectOption(u.Department,u.Department, false));
}
}
return lstOptions;
}
return null;
}
/// <summary>
/// FINDS ALL DEPARTMENTS AND ADDS THEM TO LIST OF SELECT OPTIONS
/// </summary>
/// <returns>DEPARTMENT SELECT OPTION LIST</returns>
public static List<selectOption> DepartmentKeys()
{
User[] u = [Select Department From User where Department != null and IsActive = True limit 200];
if(GlobalHelper.CheckForNotNull(u))
{
return UserInfoListHelper.AddItemToList(u);
}
return null;
}
/// <summary>
/// GETS LIST OF USERS BASED ON THE DEPARTMENT PASSED
/// </summary>
/// <param name="sInDepartment">DEPARTMENT</param>
/// <returns>USER LIST</returns>
public static List<User> UserList(string sInDepartment)
{
if(GlobalHelper.CheckForNotNull(sInDepartment))
{
try
{
User[] u = [select Name, Phone, Extension, Title, ICQ__c, Location__c,Department from User where IsActive = True AND Department =:sInDepartment Order By Name ASC limit 200];
return u;
}
catch(exception e)
{
system.debug('UserInfoListHelper - UserList - error: '+e);
}
}
return null;
}
}