September 2009 - Posts

In my previous post I introduced a small script to extend the Edit Control Block (ECB) of list items and documents. The added menu items in the ECB allow users to update certain metadata fields for that item or document. The cool thing is that everything is happening in the background with the help of jQuery, even the actual updating of the data. The result: no postbacks or full page loads, pure AJAX goodness just like showcased in the SharePoint 2010 sneak peek videos. Today I’m releasing a new and improved version of the script, based on your feedback. Here are the changes:

Column names can have a display name and an internal name

In the previous version of the script you’d had to specify the names of the columns you’d like to ajaxify based on the internal name (which escapes spaces etc). In the new version of the script you can specify the display name as well. For example this is a possible configuration for a Task list:

columns: ["Status", "Priority", "% Complete#PercentComplete", "Description#Body"]

For the Status and Priority columns, no internal names are specified since they are the same as the display names. But notice that the next two columns have a # sign in them. The part preceding the # sign is the display name, the part after the # sign is the internal name from SharePoint.

Column values can have a display name and an internal name.

This works exactly as the column names, so if there is a # sign, the first part will be the display value, the second part will be the internal value. For example this is the array of values for the % Complete column of a task list:

["100%#1.00000000000000", "75%#0.750000000000000", "50%#0.500000000000000", "25%#0.250000000000000", "0%#0"]

Internally percentages are stored as values between 0 and 1. But we’d like show them as real percentages to the user of course: e.g. 100% will be displayed, while 1.00000000000 will be stored in SharePoint.

Multiple lists and document libraries can be configured in the same script.

Now you can also use the script if there is more than one list view web part on a page: in the lists option of the ajaxListConfig variable, multiple lists can be defined based on their name.

var ajaxListConfig = {
    lists:
        [
            {   name: "Tasks",
                columns: [ ... ],
                values: [ ... ] },
            {   name: "Issues",
                columns: [ ... ],
                values: [ ... ] },
        ],
    debug: 0, // set to 1 to see log messages
    animationSpeed: "fast" // possible values: "slow", "normal", "fast" or a number (milliseconds)
};

User entered values are now supported.

In the previous version, the user could only use the predefined values for a column (which makes lots of sense for Choice columns). In the new version, if the values option of a list configuration is equal to null (instead of an array of values), dialog is displayed in which the user can fill out a new value. For example in the Task list below, the Update Description menu item is displayed, but there is submenu to display the possible values.

 

When the user clicks on the Update Description menu item, a dialog is displayed to allow the user to modify the value. Once again there are no postbacks. To display the dialog I’m using the Imprompty jQuery extension (included in my script). Notice that there is no Rich Text editing (yet), so HTML tags will be stripped out. Also there is no support for editing Person fields etc.

 

Finally ...

... some small bug fixes and performance enhancements are done. To use the script, configure it to your needs (when you download it, it’s configured for a default Task and Issue list), then follow these steps:

  1. Navigate to the page where you would like to use it (e.g. http://yoursite//Lists/Tasks/AllItems.aspx for the Task list).
  2. Click Site Actions, Edit Page (top right).
  3. Click Add a Web Part.
  4. Select the Content Editor Web Part in the Miscellaneous section and click Add.
  5. Optionally drag the Content Editor Web Part to the bottom of the screen (otherwise a small space will be displayed on top of the page).
  6. Click open the tool pane in the web part.
  7. Click Source Editor ... in the properties task pane.
  8. Copy and paste the modified script in the Text Entry dialog and click Save.
  9. Click Exit Edit Mode (top right) and verify the result.
You can download the script over here. Let me know if you have any issues and/or any feature requests!

[This script has been updated over here] I'm pretty sure every SharePoint enthusiast has seen those great Sneak Peek videos Microsoft released some time ago. And I'm sure that lots of the new features shown were very exciting for lots of you. Since SharePoint 2010 is still quite far away in the future, let's try to bring some of the 2010 stuff to SharePoint 2007! In the overview video, Tom Rizzo showed some new user interface functionality, pretty much all of it was heavily using asynchronous Javascript code to dynamically do updates, change layouts etc. All of this of course to prevent those nasty full page reloads. One of the features that caught my eye was the inline editing of list items or documents: without reloading the page, or opening a new page, it's possible in SharePoint 2010 to edit meta data. Pretty cool! And I want to have it in my SharePoint sites, today.

In my previous post I showed how you can add extra functionality to the Edit Control Block (ECB), let's take that technique and make it more flexible and customizable. Here is the result I'm looking for:

 

Notice that everything you see is happening completely at the client side, without any code deployed to the server, without any full page postbacks. So how do you get this working in your SharePoint sites? Just download this script file [This script has been updated over here] and open it. On top of the script you'll find the ajaxListConfig variable which you can change to customize the script:

var ajaxListConfig = {
    columns         :new Array("Status", "Priority"), // columns to ajaxify
    values          :new Array(
                        new Array("Not Started", "In Progress", "Completed",
    "Deferred", "Waiting on someone else"), // values for Status
                        new Array("(1) High", "(2) Normal", "(3) Low") // values for Priority
                        ),
    debug           :0, // set to 1 to see log messages
    animationSpeed  :"slow" // possible values: "slow", "normal", "fast" or a number (milliseconds)
};

When you download the script, it's configured for a default Task list. If you want to enable it on other list types or Document Libraries, or you're running SharePoint in another language than English the following options need to be changed:

  • columns: in the array you need to type the names of the columns you want to ajax-enable
  • values: for every column defined in the columns option, you need to provide an array of values

Optionally you can set the debug option to 1, so a log window is being displayed in case something goes wrong. The animationSpeed option allows you to set the speed of the fancy fade in and out effects for the updated values. When all options are set, follow these steps:

  1. Navigate to the page where you would like to use it (e.g. http://yoursite//Lists/Tasks/AllItems.aspx for the Task list).
  2. Click Site Actions, Edit Page (top right).
  3. Click Add a Web Part.
  4. Select the Content Editor Web Part in the Miscellaneous section and click Add.
  5. Optionally drag the Content Editor Web Part to the bottom of the screen (otherwise a small space will be displayed on top of the page).
  6. Click open the tool pane in the web part.
  7. Click Source Editor ... in the properties task pane.
  8. Copy and paste the modified script in the Text Entry dialog and click Save.
  9. Click Exit Edit Mode (top right) and verify the result.

Remember when the script doesn't work as expected, set the debug option to 1 and you'll see the log messages appearing at the bottom right of the screen. Btw, thanks my Twitter buddies who tested the script! (feel free to follow @jantielens).

[This script has been updated over here]

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.

 

In my two previous posts about customizing the Edit Control Block (or ECB for short) with Javascript, I showed some basic examples of what can be accomplished with this technique. Now it’s time to build a real life example instead of menu items which show Hello World dialog boxes.

I’m pretty sure everybody who is using SharePoint has sent a link to a document in a Document Library to somebody else (in an email message for example). So you probably know that links to documents (or list items) can become pretty long if the document is located in a Document Library on a site deeply buried in a hierarchy. Already some time ago the internet community has solved the issue of long URL’s (which were a pain in newsgroups for example, or too long for Twitter messages): URL shortening. The idea is pretty basic: you can request a short URL (typically containing a generated code) for a long URL. When somebody uses the short URL, the long URL is retrieved and the user is redirected. There are many sites on the internet that provide this service for free, in this article I will be using bit.ly because they have a pretty nice API you can work with as a developer. For example this link http://bit.ly/1btOBN will navigate to http://weblogs.asp.net/jan/archive/2009/09/04/customizing-the-sharepoint-ecb-with-javascript-part-2.aspx.

So what’s the idea? Instead of having to copy the link of a SharePoint document or list item to the clipboard, navigate to bit.ly and request a short URL over there; we’ll build this functionality directly into the SharePoint web user interface. The screenshot below shows the ECB of a normal Document Library, as you can see there is an extra menu item Shorten Link with bit.ly that has three child menu items.

Once again, in my previous post I go into the details about how you can build a nested ECB menu item by just using Javascript. So let’s quickly discuss the code to build the menu items:

function Custom_AddDocLibMenuItems(m, ctx) {
    var itemLink = window.location.protocol + "//" + window.location.host +
                        GetAttributeFromItemTable(itemTable, "Url", "ServerUrl");
    return addBitlyMenuItems(m, ctx, itemLink);
}

function Custom_AddListMenuItems(m, ctx) {
    var itemLink = window.location.protocol + "//" + window.location.host +
                        ctx.displayFormUrl + "?ID=" + currentItemID;
    return addBitlyMenuItems(m, ctx, itemLink);
}

function addBitlyMenuItems(m, ctx, url) {
    var shortenMenu = CASubM(m,"Shorten Link with bit.ly", "/_layouts/images/RAT16.GIF");
    
    CAMOpt(shortenMenu, "Shorten and Display", "getShortUrl('" +
            url + "', shortenAndDisplay)", "");
    
    CAMOpt(shortenMenu, "Shorten and Copy to Clipboard",
            "getShortUrl('" + url + "', shortenAndCopy)");
    
    CAMSep(shortenMenu);  // separator
    
    CAMOpt(shortenMenu, "Show Statistics for Short Link",
            "getShortUrl('" + url + "', shortenAndShowStats)", "/_layouts/images/GRA16.GIF");

    CAMSep(m); // separator
    return false;
}

Because I want to show the ECB menu item both in Lists and Document Libraries, I’ve implemented both the Custom_AddDocLibMenuItems and Custom_AddListMenuItems Javascript functions. In those functions a string is constructed that contains a link to either the list item or to the document. Once we’ve got that URL, the addBitlyMenuItems function is called. This function will build the parent menu item in the ECB and the three child menu items which will actually do the actions. Every action menu item calls a custom Javascript function called getShortUrl which has a parameter for the long URL and a callback function. The getShortUrl function builds an URL to call the bit.ly REST API. This URL is called with the help of the jQuery getJSON function. Note that to make use of the bit.ly API you need to have a free account and a corresponding key.

function getShortUrl(url, callback) {
    var bitlyURL = "http://api.bit.ly/shorten?"
        + "version=2.0.1"
        + "&longUrl=" + escape(url)
        + "&login=YOUR_LOGIN" +
        + "&apiKey=YOUR_KEY" +
        + "&history=1" +
        + "&format=json&callback=?";

    $.getJSON(bitlyURL, function(data){
        callback(data.results[url].shortUrl);
    });
}

Once the getJSON function receives the data from bit.ly, the callback function (which was passed as a parameter) is called, passing along the shortened URL.

The three callback functions used in the addBitlyMenuItems function, will each do a specific action: show the shortened link, copy the link to the clipboard or navigate to the bit.ly statistics page. The shortenAndDisplay is the most complex of those three; this function will display the shortened URL in a DIV centered on top of the page. Once again jQuery is used for manipulating the HTML DOM to dynamically render the DIV.

function shortenAndDisplay(url) {
    $("body").append("<div id='shortUrlDiv' class='ms-vb' style='position:absolute; \
        background-color:White; padding:10px; border-width:1px; border-style:solid; \
        border-color:Black;display:none; position:absolute;'>URL shortened to:<h2>" +
        url + "</h2><a href='#' id='shortUrlDivClose'>Close</a></div>");
    
    var $shortUrlDiv = $("#shortUrlDiv");

    var topPos = (document.body.clientHeight - $shortUrlDiv.height()) / 2;
    var leftPos = (document.body.clientWidth - $shortUrlDiv.width()) / 2;
    
    $("#shortUrlDivClose", $shortUrlDiv).click(function() {
        $(this).parent().hide("fast", function() { $(this).remove(); });
    });
    
    $("#shortUrlDiv").css("top", topPos).css("left", leftPos).show("fast");
}


The result of this function looks like this:

 

The other two callback functions are very easy to implement. Notice that to show the bit.ly statistics page for a shortened link, you just need to add a + sign to that link.

function shortenAndCopy(url) {
    clipboardData.setData("Text", url);
}
function shortenAndShowStats(url) {
    window.location = url + "+";
}

The complete source code can be downloaded from here. You will notice that I’ve made the code somewhat easier to configure by adding a configuration variable at the top:

var shortenConfig = {
    debug: 0,                   // set this value to 1 to see debug information
    bitlyLogin: "spdemo",       // bit.ly account, below API key
    bitlyAPIKey:"R_877718dd86f418d0ee840c08af717a68",
    bitlyHistory: 1             // set this value to 1 to add to bit.ly history
};

When you set the debug variable to 1, another DIV is dynamically added to the page and will show some log messages. When the bitlyHistory variable is set to 1, bit.ly will add the shortened link to the bit.ly account (so you can track it). Once again, if you plan to use this, go to bit.ly and sign up to get your own login and API key. On top of my code there is a reference to the jQuery library hosted by Google. If you have deployed jQuery locally, feel free to change the script reference. To use the code, just navigate to a Document Library or List, and add a new Content Editor web part to that page. As the HTML source of the web part, copy and paste the entire code that you've downloaded. (For a detailed description how to add the Content Editor web part, check an earlier article)

Other articles in this series:


In the previous article in this series I explained how to add menu items in the Edit Control Block of Lists and Document Libraries. The menu items added so far are all displayed at the same level; directly in the ECB. It is possible however to build hierarchical menus (menu items with sub menu items) using this technique as well. A good example of a hierarchical menu in the ECB is the default Send To menu:

 

Actually this is very easy to accomplish, instead of the CAMOpt Javascript function discussed in the previous post, the function CASubM can be used to create fly-out menu items (the ‘parent’ menu items). The following code snippet makes use of this technique:

function Custom_AddListMenuItems(m, ctx) {
    var menuItem = CASubM(m,"Menu Item", "/_layouts/images/LWV16.GIF")
    
    CAMOpt(menuItem, "Sub Menu Item 1", "alert('Hello World!');");
    CAMOpt(menuItem, "Sub Menu Item 2", "alert('Hello World!');");

    var subMenuItem = CASubM(menuItem,"Sub Menu Item 3")
    CAMOpt(subMenuItem, "Sub Menu Item 4", "alert('Hello World!');");
    CAMOpt(subMenuItem, "Sub Menu Item 5", "alert('Hello World!');");

    CAMSep(m); // separator
    return false; // render the default menu items too
}

When this code is dropped in a Content Editor web part on a List page for example (see the previous article for the steps to follow), the rendered ECB will have the Menu Item at the first level. This item contains three child menu items; the third child on itself contains two additional child menu items.

 

As already mentioned the key to this technique is the CASubM Javascript function. The return value is a reference to the menu item that is created; it is this instance that is used as a parameter of the CAMOpt function to specify on which level the menu item should be created. The CASubM function can have 6 parameters (the first two are required):

  1. a reference to the parent menu (just like the CAMOpt function)
  2. the text to display
  3. the URL of an icon for the menu item
  4. the HTML alt attribute value of the icon
  5. the HTML sequence attribute value of the menu item
  6. the HTML description attribute value of the icon

The code discussed above is a basic sample that does nothing more than displaying a Hello World message box when one of the list items is clicked. In real life you probably want to do something interesting when the user clicks on an ECB menu item; let’s try to create the following ECB:

 

The code to render the Open in New Window menu item, including the sub menu items, goes as follows:

function Custom_AddListMenuItems(m, ctx) {
    var openMenu = CASubM(m,"Open in New Window");
    
    CAMOpt(openMenu, "View Item", "alert('Todo');");
    
    CAMOpt(openMenu, "Edit Item",
            "alert('Todo');", "/_layouts/images/edititem.gif");
   
    return false; // render the default menu items too
}

So far nothing new or exciting, the challenge however is to come up with the Javascript functions which will be called when the View Item or Edit Item sub menu items are clicked (now they just display a dialog box). The issue (or challenge) is that the Javascript function needs to now for which item in the List or Document Library the ECB is rendered. Once again the core.js is helping us out; it contains code that populates some context information about these items. There is for example a variable in the core.js called currentItemID which contains the ID of  the List item or Document for which the ECB is being rendered. Some other interesting variables at the same location are: currentItemIcon, currentItemFileUrl, currentItemCheckedOutUserId and currentItemModerationStatus. For a full list, just open the core.js file in the \12 hive and you’ll see all of them at the top. Besides those variables there is also an object called ctx with even more information about the List or Document Library itself. The ctx object is defined in the init.js, and has properties like listName, view, displayFormUrl etc. So if we adopt the code to build some URL’s which will be opened in a new window, the work is done.

function Custom_AddListMenuItems(m, ctx) {
    var viewURL = window.location.protocol + "//" + window.location.host +
                        ctx.displayFormUrl + "?ID=" + currentItemID;
    
    var editURL = window.location.protocol + "//" + window.location.host +
                        ctx.editFormUrl + "?ID=" + currentItemID;                        

    var openMenu = CASubM(m,"Open in New Window");
    
    CAMOpt(openMenu, "View Item", "window.open('" + viewURL + "');");
    
    CAMOpt(openMenu, "Edit Item",
            "window.open('" + editURL + "');", "/_layouts/images/edititem.gif");
   
    return false; // render the default menu items too
}

Using the variables found in the core.js and init.js files, you have access to the most basic set of metadata. But quite often it’s necessary to have access to even more metadata like non default fields etc. In the next article in this series, I’ll show you how this can be accomplished.

Other articles in this series:

First of all; what is the ECB in SharePoint? Well ECB stands for Edit Control Block and it is the context menu that is displayed for all items in SharePoint Lists and Document Libraries (and their corresponding web parts). The menu items shown below are the default ones that you get out-of-the-box, in this case in a List.

 

Probably the most commonly used technique to customize the ECB is by making use of a Feature that contains a CustomAction element, as displayed below:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <CustomAction
        Id="{B0B5A0CB-7FBE-4dd6-9B2A-2B1E1321B8F9}"
        RegistrationType="List"
        RegistrationId="101"
        Location="EditControlBlock"
        Title="Dummy Menu Item">
    <UrlAction
        Url="/_layouts/dummy.aspx"/>   
    </CustomAction>
</Elements>

By using the EditControlBlock value for the Location attribute, the Dummy Menu Item is added to the ECB in all the Document Libraries (the RegistrationId attribute value of 101). This technique works of course, but there are some limitations related to it. First of all when a menu item is added to the ECB by using a CustomAction element, the menu item is displayed in all ECB menus; there is no way to make the menu item context sensitive. It could be for example that you only want to show the menu item for some of the documents, not all of them. The out-of-the-box Check In/Out behaviour is a good example of this: Check In is only displayed when the document is in the Checked Out state and visa versa. A second limitation is that using this technique, it’s not possible to create nested menu items, like the out-of-the-box Send To ECB menu item for example. The underlying reason is that unfortunately it is not possible to use the ControlAssembly and ControlClass attributes in the CustomAction element of a feature, when the location is EditControlBlock. When using the ControlAssembly and ControlClass attributes you can basically point to a server control that's responsible for rendering the UI to show as the Custom Action in the ECB. More information and an example can be found in one of my previous posts Creating Hierarchical Menus with a Custom Action. But once again, this will not work in the ECB!

Luckily there is another technique to customize the ECB that solves these issues, this technique is using Javascript. The key functionality to build custom ECB menu items is pretty easy since SharePoint already contains some wrapper Javascript functions to do this. In the core.js file (which can be found in C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\1033) you can find the methods AddListMenuItems and AddDocLibMenuItems. These methods are called by SharePoint when an ECB should be built for a List item or Document. Although technically it is possible to modify those functions directly in the core.js, it is not supported. There is a much nicer mechanism available to accomplish this! The first lines of code you can find in the AddListMenuItems function look like this (the AddDocLibMenuItems function contains similar code):

if (typeof(Custom_AddListMenuItems) !="undefined")
{
    if (Custom_AddListMenuItems(m, ctx))
        return;
}

Basically the Javascript function verifies if there is a function known called Custom_AddListMenuItems, if so this function is called. The return value of this function (true or false) determines if the rest of the AddListMenuItems function is executed; thus if the default menu items are added or not. Out-of-the-box there is no function called Custom_AddListMenuItems, the goal is that you build on yourself. Once again you can define this function directly in the core.js file, but this is unsupported. The nice thing is that this function can be defined in other .js files or even directly in SharePoint pages. Before we do that, let’s find out the code to implement a custom Custom_AddListMenuItems function:

function Custom_AddListMenuItems(m, ctx) {
    CAMOpt(m, "Dummy link", "alert('Hello World!');", "/_layouts/images/LWV16.GIF");
    CAMSep(m);
}

The first line calls the CAMOpt function (which is available in the core.js as well) to create a new menu item. As you can verify in the core.js, the CAMOpt function can have seven parameters (the first three are required):

  1. a reference to an ECB, typically passed by the caller of Custom_AddListMenuItems
  2. the text to display
  3. the Javascript function to execute, in this sample a basic Hello World dialog is shown
  4. the URL of an icon for the menu item
  5. the HTML alt attribute value of the icon
  6. the HTML description attribute value of the icon

On the second line the CAMSep function is called which renders a separator (a thin line).

The easiest way to test this code is to navigate to a SharePoint list, for example a Task list (e.g. http://yoursite/Lists/Tasks/AllItems.aspx). In the Site Actions menu of that page choose Edit Page. Notice that this page is having one Web Part Zone called Main, which already displays one web part. Add a new Content Editor web part to the Main zone; and choose Modify Shared Web Part from the Edit menu of the newly added web part (top right). In the properties task pane of the web part, click on the Source Editor button and copy/paste the entire AddListMenuItems function. When the page is reloaded, the result should look like this:

 

When the new Dummy link menu item is clicked; a simple dialog box will be shown.

This is the first part of a series of posts which will show you how to customize the Edit Control Block by using Javascript. In the next post I’ll show you how you can create nested (or hierarchical) menu’s in the ECB.

Update: I added a few lines which explicitly specify that the ControlClass and ControlAssembly attributes will not work in the ECB. Thanks Wouter for mentioning this!

More Posts