Customizing the SharePoint ECB with Javascript, Part 3

Other articles in this series:

In the previous articles I explained the basic technique to add custom menu items to the Edit Control Block (ECB) using Javascript. Basically it comes down to writing a Javascript function called Custom_AddListMenuItems or Custom_AddDocLibMenuItems (respectively for adding menu items to the ECB of Lists and Document Libraries). In these custom functions you can use the CAMOpt Javascript function (found in the default core.js file) to add as many items as you want. Using the CASubM function you can also build hierarchical menus.

The next thing that I would like to discuss is how to create "context sensitive menu items" in the ECB using these techniques. What do I mean with "context sensitive"? Let’s take a look at the out-of-the-box ECB of a Document Library. In that ECB a menu item is displayed to Check Out a document. But this menu item is only displayed when the document is not yet checked out. When the document is checked out, the ECB displays the Check In and Discard Check Out menu items instead. So based on the metadata of the document, the ECB is different in this scenario.

     

To illustrate how you can build context sensitive ECB menu items yourself, let’s take the following example: we’ll enhance the ECB of a default Task list so it shows menu items to quickly mark a task as Completed, In Progress etc. In the following screenshot the ECB for Test Task 1 is displayed. The Status of that task is set to Not Started, so the Update Status menu item only displays In Progress, Complete, Deferred and Waiting child menu items.

 

In the same task list, there is also a Test Task 2 item for which the status is set to In Progress. The Update Status menu item in the ECB now displays Not Started, Complete, Deferred and Waiting.

 

Since the customized ECB is configured in a Javascript function (Custom_AddListMenuItems in this case), we need to be able to retrieve the Status value of the item for which the ECB is currently being rendered. The core.js and init.js out-of-the-box Javascript files, give us little meta data information: only the item ID, checked out status etc are available. In this case technically it is possible to retrieve the Status value of the list item by querying the HTML DOM. Although this technique would work, it would only work if the needed meta data is actually displayed. E.g. if the Status column would not be displayed in the view, the information could not be retrieved. Therefore I’m using another technique that retrieves the necessary meta data information by making a call to the lists.asmx web service. This web service has a web method called GetListItems that can retrieve all meta data for one or more list items (as described in an earlier post). Once we have the meta data, the rendering of the ECB menu items is easy:

function Custom_AddListMenuItems(m, ctx) {
    var soapEnv =
        "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'> \
            <soapenv:Body> \
                 <GetListItems xmlns='http://schemas.microsoft.com/sharepoint/soap/'> \
                    <listName>" + ctx.listName + "</listName> \
                    <viewFields> \
                        <ViewFields> \
                           <FieldRef Name='Status' /> \
                       </ViewFields> \
                    </viewFields> \
                    <query> \
                        <Query><Where> \
                            <Eq> \
                                <FieldRef Name='ID' /> \
                                <Value Type='Integer'>" + currentItemID + "</Value> \
                            </Eq> \
                        </Where></Query>\
                    </query> \
                </GetListItems> \
            </soapenv:Body> \
        </soapenv:Envelope>";

    var wsurl = ctx.HttpRoot + "/_vti_bin/lists.asmx";
    
    $.ajax({
        async: false,
        url: wsurl,
        type: "POST",
        dataType: "xml",
        data: soapEnv,
        complete: function(xData, status) {
            var status = $(xData.responseXML).find("z\\:row:eq(0)").attr("ows_Status");
            
            var menuItem = CASubM(m,"Update Status");

            var statusOptions = new Array("Not Started", "In Progress",
                    "Completed", "Deferred", "Waiting on someone else");

            for(var i in statusOptions) {
                var statusOption = statusOptions[i];
                if(statusOption  != status)
                    CAMOpt(menuItem, statusOption ,
                    "changeTaskStatus('" + wsurl + "','" + ctx.listName + "','" +
                    currentItemID + "','" + statusOption + "');");
            }
        },
        contentType: "text/xml; charset=\"utf-8\""
    });
    
    CAMSep(m);
    return false;
}

First the SOAP Envelope to send to the GetListItems web method is constructed (soapEnv variable); it uses the listName property of the ctx object (defined by SharePoint in the init.js), and the currentItemID (defined by SharePoint in the core.js). The jQuery ajax method is used to make a the web service call. When the data is retrieved (the complete option of the ajax method) the current Status value is extracted and stored in the status variable. Next a new menu item is added to the ECB using the CASubM function (since this menu item will contain child items). Finally there is a loop over all possible Status values (stored in the statusOptions array); every possible Status is added as a sub menu, except the Status that is currently assigned to the task item. Notice that the third parameter of the CAMOpt function is a Javascript call to the changeTaskStatus function, passing the web service URL, the list ID, the item ID and the selected status as parameters.

The changeTaskStatus function is pretty straight forward: once again a web service call is made, this time to the UpdateListItems web method of the lists.asmx web service. The SOAP Envelope sent to this web method contains a Batch element in which an update is described of the Task list item.

function changeTaskStatus(wsurl, list, itemid, newstatus) {
    var batch =
        "<Batch OnError=\"Continue\"> \
            <Method ID=\"1\" Cmd=\"Update\"> \
                <Field Name=\"ID\">" + itemid + "</Field> \
                <Field Name=\"Status\">" + newstatus + "</Field> \
            </Method> \
        </Batch>";

    var soapEnv =
        "<?xml version=\"1.0\" encoding=\"utf-8\"?> \
        <soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \
            xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" \
            xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"> \
          <soap:Body> \
            <UpdateListItems xmlns=\"http://schemas.microsoft.com/sharepoint/soap/\"> \
              <listName>" + list + "</listName> \
              <updates> \
                " + batch + "</updates> \
            </UpdateListItems> \
          </soap:Body> \
        </soap:Envelope>";

    $.ajax({
        url: wsurl,
        beforeSend: function(xhr) {
            xhr.setRequestHeader("SOAPAction",
            "http://schemas.microsoft.com/sharepoint/soap/UpdateListItems");
        },
        type: "POST",
        dataType: "xml",
        data: soapEnv,
        complete: function(xData, result) {
            window.location.href=window.location.href;
        },
        contentType: "text/xml; charset=utf-8"
    });    
}

When the web service call is done (the complete option of the ajax method) the page is refreshed by setting the href of the window.location property to it’s current value. As a result the page will display the updated value of the Status.

Although this is pretty cool, it’s a pity of course that everything is happening using client side Javascript; except updating the value of the Status column in the HTML page (which done with a full page refresh). Let’s try to fix this! To get to the location in the HTML DOM where the Status of the Task item is displayed is quite complex due to how SharePoint generates the HTML for a List view. The getItemTD function below will get a reference to the TD element for a specific Task item. First of all this method selects a table element with the ID set to the combination of the list ID and view ID. Notice that this table ID needs to be escaped for jQuery to make the selection. Next the TR element (row) is selected for the Task item, based on the ID of a table nested on the TR which is the same as the item ID. After that the header row is selected so the index of the Status column can be calculated. Using this index the correct TD element (table cell) is selected and returned.

function getItemTD(itemid) {
    var tableid = ctx.listName + "-" + ctx.view;
    
    // escape the table id ({ and } should become \{ and \}
    tableid = tableid.replace(/{/g, "\\{").replace(/}/g, "\\}");

    // select them TR for the item
    $itemrow = $("#" + tableid + " table[id='" + itemid + "']").parent().parent();
    
    // select the header row
    $headerrow = $(">tr:eq(0)", $itemrow.parent());

    // select the table in the header row for the specified column
    $idtable = $("th>div>table[Name='Status']", $headerrow);
    
    // calculate the index of the column, based on the idtable
    var columnIndex =$(">th",$headerrow).index($idtable.parent().parent());
    
    // based on the index, let's get the TD
    return $(">td:eq(" + columnIndex + ")", $itemrow);
}

To make us of this function the complete option of the ajax function, used in the changeTaskStatus function, has to be changed:

...
complete: function(xData, result) {
    getItemTD(itemid).text(newstatus);
},
...

The getItemTD function is used to select the table cell which should be updated with the new value. The result is that now the Status of a Task item can be updated, both in the SharePoint database and the web UI, without full page postbacks!

You can download the full source code of this sample over here. To use it, navigate to a default Task list (e.g. http://yoursite/Lists/Tasks/AllItems.aspx) and add a Content Editor Web Part to the page. Open the Tool Pane of the Content Editor Web Part and paste the downloaded script in the Source property of the web part (detailed instructions to add the web part can be found over here). Notice that on top of the script a reference is made to the jQuery library hosted by Google. Once again, if you host the jQuery library yourself (e.g. in a Document Library), feel free to update the URL.

 

6 Comments

  • Jan- great post. Have you heard of or tried egnyte? Egnyte is a sharing, backup, and collaboration. Its target is SMB.

  • Nice Post!!!

    I tried adding custom action using your approach, bu the menu only show up in the default view, whn i swirch the view the custom actions are gone!!!

    Can you tell me why is this happening and how do I fix this??

  • I figured it out!!!

    Actually evry view is andifferent aspx page, so you need to add the content editor on evry page to make this work....

    Thanks for the nice post..

  • Is there a way to hide a menu item, such as Check Out / Check In or Workflows? Icing on the cake would be to hide or show based on permissions...

    Thanks for your help and your great blog!

  • ♥♥♥
    thanks
    for your great solution !!!!

  • Great post! thanks!

    This was exact what i was looking for. I'd like to add one comment: if getItemTD() results in an element, you could even skip the first ajax roundtrip for looking up the current status. In that case, you'd only need the update call.

Comments have been disabled for this content.