Hiding SharePoint ECB Menu Items for a Specific List


While prototyping an Approval process against listitems for a client, I had a need to remove (hide) the Approve/Reject ECB menu item for items in a specific list.

Standard Listitem ECB Menu
Standard Listitem ECB Menu

Using the OOTB SharePoint 2010 Content Approval Workflow with Listitems.

This workflow can be used to manage content approval for Listitems, but there is a caveat. The Workflow will not set the Approval (Moderation) status column of the workflow listitem if the Workflow has been configured to start automatically on item change.
This makes sense, since it would cause a new workflow instance to be started. This means to use this workflow to manage content approval in this way, you have to configure the workflow to not start automatically on item change and a user must start it manually.

This causes a problem for listitems in particular, because the ECB menu contains the Approve/Reject item which would allow a user to circumvent the workflow based approval process, so to work around this I wanted to hide this ECB menu item but only for items in a specific list.

Most of the options suggested by google involve editing the CORE.js which I didn’t want to do, the solution must be deployed and activatable in a supportable way, and be able to handle the ECB menu built when a user is using a list view and also when a list view webpart has been added to a page.

Reference;

So briefly my solution involves;

  • A script running on every page which examines the ContextInfo and using the ListTitle property to determine whether it needs to override the default List menu items.
  • A ScriptLink type CustomAction to introduce the script to every page
  • A Module to provision the script into the Site Assets library in the root web of the site collection

First the custom action;

<CustomAction Id="6e759e97-3854-4fd1-876b-6d72d4559b12" 
					  Title="HideApproveRejectECBMenuItem" 
					  Location="ScriptLink"
					  ScriptSrc="~sitecollection/SiteAssets/HideApproveRejectECBMenuItem.js" 
					  Sequence="100"/>

The Site Assets module;

<Module Name="SiteAssets" Path="SiteAssets" Url="SiteAssets">
		<File Path="HideApproveRejectECBMenuItem.js" 
				Url="HideApproveRejectECBMenuItem.js" 
				IgnoreIfAlreadyExists="TRUE" 
				Type="GhostableInLibrary" />
	</Module>

The first part of the script is a function which checks if the ContextInfo object passed to it is the ContextInfo object for the list we are interested in ~ in this case the function is being called by the Custom_AddListMenuItems() function. If the function is called without arguments, it examines the g_ctxDict variable (an array of ContextInfo objects) looking for a ContextInfo object for the list we are interested in.

function CanExecuteHideForECBMenuItem(ctx) {
	if (typeof (ctx) != 'undefined' && ctx) {
		/* passed a contextinfo for a specific list, 
		   check its the list we're interested in */
		if (ctx.ListTitle === "Approval Test List") return true;
		return false;
	}

	/* not passed a specific contextinfo, so check the array of contextinfo's 
	   which gets created whenever you're in a listview or on a page with 
		list view webparts on */
	if (typeof (g_ctxDict) === 'undefined' || !g_ctxDict) return false;
	var lctx = null;
	for (var p in g_ctxDict) {
		if (g_ctxDict[p].ListTitle === "Approval Test List") {
			lctx = g_ctxDict[p];
			break;
		}
	}
	return (lctx != null);
}

The next part of the script will remove ECB menu items introduced via feature activation (Custom Actions) ~ these get added to the page as embedded HTML markup, and this function will remove them as required so that they can’t be added to the ECB.

function HideFeatureActivatedECBMenuItem() {
	if (!CanExecuteHideForECBMenuItem()) return;

	/* find the ECB menu items which are added via feature activation */
	var ecbId = "ECBItems";
	var listName;
	if ((null != ctx.listName) && (0 < ctx.listName.length)) {
		listName = ctx.listName.toLowerCase();
		ecbId = ecbId + "_" + listName;
	}
	var elemTBody = document.getElementById(ecbId);
	if (elemTBody != null) {
		// iterate each table row to find the ECB menu item to remove (hide)
		for (var iMenuItem = 0; iMenuItem < elemTBody.childNodes.length; iMenuItem++) {
			var elemTr = elemTBody.childNodes[iMenuItem];
			var elemTdTitle = elemTr.childNodes[0];
			var title = GetInnerText(elemTdTitle);

			// check the ECB item title
			if (title === "Title of ECB Menu Item to Hide") {
				elemTBody.removeChild(elemTr);
			}
		}
	}
}

The next part of the code is a bit contentious ~ there seems to be no way to stop individual OOTB ECB menu items from being added to the ECB (security aside) since they are added by the AddListMenuItems function in CORE.js, this function is called when you first click the drop-down arrow for a listitems ECB control, and, there is a hook into this function.

By declaring a function named Custom_AddListMenuItems you can hook into and completely override the way an ECB is built for list items, and by returning either true/false from the custom function you can control whether the AddListMenuItems function executes its own code (builds the OOTB menu items) or not (meaning your code has built the ECB menu).

In my scenario, the Custom_AddListMenuItems function is implemented such that, if  the ECB is being built for items in the list we are interested in it will build the standard ECB (minus the bits we don’t want) using the code copied from CORE.js and return true, otherwise it will return false, indicating that the AddListMenuItems function should execute its own code as normal (and build a standard ECB menu).

This copying of code from CORE.js is the bit I don’t like, but there seems no other way of doing it.

/* return true to prevent OOTB AddListMenuItems() from executing its code
   else return false to cause AddListMenuItems() to execute its code */
function Custom_AddListMenuItems(m, ctx) {
	/* check that the ECB is being built for the list we're interested in */
	if (!CanExecuteHideForECBMenuItem(ctx)) return false;

	/* remainder is OOTB code taken from CORE.debug.js line 6757 
		with the exeception of not adding the Approve/Reject menu item
		and returning true at function end
	*/
	if (currentItemFileUrl==null)
		currentItemFileUrl=GetAttributeFromItemTable(itemTable, "Url", "ServerUrl");
	var currentItemEscapedFileUrl;
	if (currentItemFileUrl !=null)
		currentItemEscapedFileUrl=escapeProperly(unescapeProperly(currentItemFileUrl));
	var fixedItemId=currentItemID;
	if (currentItemIsEventsExcp==null)
	{
		currentItemIsEventsExcp=false;
		currentItemIsEventsDeletedExcp=false;
		currentItemEvtType=itemTable.getAttribute("EventType");
		if(currentItemEvtType !=null &&
			 (currentItemEvtType==2 || currentItemEvtType==3 || currentItemEvtType==4))
		{
			currentItemIsEventsExcp=true;
			if (currentItemEvtType==3)
				currentItemIsEventsDeletedExcp=true;
			if (currentItemID.indexOf(".") !=-1)
				fixedItemId=currentItemID.split(".")[0];
		}
	}
	var menuOption;
	if (ctx.listBaseType==3 && ctx.listTemplate==108)
	{
		strDisplayText=L_Reply_Text;
		if(itemTable.getAttribute("Ordering").length>=504)
		{
			var L_ReplyLimitMsg_Text="Cannot reply to this thread. The reply limit has been reached.";
			strAction="alert('"+L_ReplyLimitMsg_Text+"')";
		}
		else
		{
			strAction="STSNavigate('"+ctx.newFormUrl
+"&Threading="+escapeProperly(itemTable.getAttribute("Ordering"))
+"&Guid="+escapeProperly(itemTable.getAttribute("ThreadID"))
+"&Subject="+escapeProperly(itemTable.getAttribute("Subject"));
			strAction=AddSourceToUrl(strAction)+"')";
		}
		strImagePath=ctx.imagesPath+"reply.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 100);
		menuOption.id="ID_Reply";
	}
	AddSharedNamespaceMenuItems(m, ctx);
	var contentTypeId=itemTable.getAttribute("CId");
	if (contentTypeId !=null && contentTypeId.indexOf("0x0106")==0
			&& HasRights(0x10, 0x0))
	{
		strDisplayText=L_ExportContact_Text;
		strAction="STSNavigate('"+ctx.HttpPath+"&Cmd=Display&CacheControl=1&List="+ctx.listName+"&ID="+currentItemID+"&Using="+escapeProperly(ctx.listUrlDir)+"/vcard.vcf"+"')";
		strImagePath=ctx.imagesPath+"exptitem.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 350);
		CUIInfo(menuOption, "ExportContact", ["ExportContact"]);
		menuOption.id="ID_ExportContact";
	}
	CAMSep(m);
	if (ctx.verEnabled==1)
	{
		AddVersionsMenuItem(m, ctx, currentItemEscapedFileUrl);
	}
	/* Dont add the Approve/Reject ECB menu item
	if (ctx.isModerated==true &&
		HasRights(0x0, 0x10) && HasRights(0x0, 0x4) &&
		HasRights(0x0, 0x21000) && ctx.listBaseType !=4 &&
		currentItemID.indexOf(".0.") < 0)
	{
		strDisplayText=L_ModerateItem_Text;
		strAction="NavigateToApproveRejectAspx(event, '"+ctx.HttpRoot+"/_layouts/approve.aspx?List="+ctx.listName
+"&ID="+fixedItemId;
		strAction=AddSourceToUrl(strAction)+"')";
		strImagePath=ctx.imagesPath+"apprj.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 850);
		CUIInfo(menuOption, "Moderate", ["Moderate"]);
		menuOption.id="ID_ModerateItem";
	}
	CAMSep(m);
	*/
	AddWorkflowsMenuItem(m, ctx);
	var alertsEnabled=typeof(_spPageContextInfo) !='undefined' && _spPageContextInfo !=null && _spPageContextInfo.alertsEnabled;
	if ((currentItemID.indexOf(".0.") < 0)
		  && HasRights(0x80, 0x0)
		  && !ctx.ExternalDataList
		  && alertsEnabled)
	{
		strDisplayText=L_Subscribe_Text;
		strAction="NavigateToSubNewAspxV4(event, '"+ctx.HttpRoot+"', 'List="+ctx.listName+"&ID="+currentItemID+"')";
		strImagePath="";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1100);
		menuOption.id="ID_Subscribe";
		CUIInfo(menuOption, "Subscribe", ["Subscribe"]);
	}
	if (alertsEnabled || (ctx.WorkflowsAssociated && HasRights(0x0, 0x4)))
	{
		CAMSep(m);
	}
	AddManagePermsMenuItem(m, ctx, ctx.listName, currentItemID);
	if (currentItemID.indexOf(".0.") < 0 && HasRights(0x0, 0x8)
		  && !currentItemIsEventsExcp)
	{
		if (ctx.listBaseType==4)
			strDisplayText=L_DeleteResponse_Text;
		else
			strDisplayText=L_DeleteItem_Text;
		strAction="DeleteListItem()";
		strImagePath=ctx.imagesPath+"delitem.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);
		CUIInfo(menuOption, "Delete", ["Delete"]);
		menuOption.id="ID_DeleteItem";
		CUIInfo(menuOption, "Delete", ["Delete"]);
	}
	var hasProgId=(currentItemProgId !=null) && (currentItemProgId !="");
	if (currentItemFSObjType==1 &&
		!hasProgId &&
		ctx.ContentTypesEnabled &&
		ctx.listTemplate !=108)
	{
		strDisplayText=L_CustomizeNewButton_Text;
		strAction="STSNavigate('"+ctx.HttpRoot+"/_layouts/ChangeContentTypeOrder.aspx?List="+ctx.listName+"&RootFolder="+currentItemEscapedFileUrl;
		strAction=AddSourceToUrl(strAction)+"')";
		strImagePath="";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1170);
		CUIInfo(menuOption, "ChangeNewButton", ["ChangeNewButton"]);
		menuOption.id="ID_CustomizeNewButton";
	}
	return true;
}

The complete script is as follows;

function CanExecuteHideForECBMenuItem(ctx) {
	if (typeof (ctx) != 'undefined' && ctx) {
		/* passed a contextinfo for a specific list, 
		   check its the list we're interested in */
		if (ctx.ListTitle === "Approval Test List") return true;
		return false;
	}

	/* not passed a specific contextinfo, so check the array of contextinfo's 
	   which gets created whenever you're in a listview or on a page with 
		list view webparts on */
	if (typeof (g_ctxDict) === 'undefined' || !g_ctxDict) return false;
	var lctx = null;
	for (var p in g_ctxDict) {
		lctx = g_ctxDict[p];
		if (lctx.ListTitle === "Approval Test List") break;
	}
	return (lctx != null);
}

function HideFeatureActivatedECBMenuItem() {
	if (!CanExecuteHideForECBMenuItem()) return;

	/* find the ECB menu items which are added via feature activation */
	var ecbId = "ECBItems";
	var listName;
	if ((null != ctx.listName) && (0 < ctx.listName.length)) {
		listName = ctx.listName.toLowerCase();
		ecbId = ecbId + "_" + listName;
	}
	var elemTBody = document.getElementById(ecbId);
	if (elemTBody != null) {
		// iterate each table row to find the ECB menu item to remove (hide)
		for (var iMenuItem = 0; iMenuItem < elemTBody.childNodes.length; iMenuItem++) {
			var elemTr = elemTBody.childNodes[iMenuItem];
			var elemTdTitle = elemTr.childNodes[0];
			var title = GetInnerText(elemTdTitle);

			// check the ECB item title
			if (title === "Title of ECB Menu Item to Hide") {
				elemTBody.removeChild(elemTr);
			}
		}
	}
}

/* return true to prevent OOTB AddListMenuItems() from executing its code
   else return false to cause AddListMenuItems() to execute its code */
function Custom_AddListMenuItems(m, ctx) {
	/* check that the ECB is being built for the list we're interested in */
	if (!CanExecuteHideForECBMenuItem(ctx)) return false;

	/* remainder is OOTB code taken from CORE.debug.js line 6757 
		with the exeception of not adding the Approve/Reject menu item
		and returning true at function end
	*/
	if (currentItemFileUrl==null)
		currentItemFileUrl=GetAttributeFromItemTable(itemTable, "Url", "ServerUrl");
	var currentItemEscapedFileUrl;
	if (currentItemFileUrl !=null)
		currentItemEscapedFileUrl=escapeProperly(unescapeProperly(currentItemFileUrl));
	var fixedItemId=currentItemID;
	if (currentItemIsEventsExcp==null)
	{
		currentItemIsEventsExcp=false;
		currentItemIsEventsDeletedExcp=false;
		currentItemEvtType=itemTable.getAttribute("EventType");
		if(currentItemEvtType !=null &&
			 (currentItemEvtType==2 || currentItemEvtType==3 || currentItemEvtType==4))
		{
			currentItemIsEventsExcp=true;
			if (currentItemEvtType==3)
				currentItemIsEventsDeletedExcp=true;
			if (currentItemID.indexOf(".") !=-1)
				fixedItemId=currentItemID.split(".")[0];
		}
	}
	var menuOption;
	if (ctx.listBaseType==3 && ctx.listTemplate==108)
	{
		strDisplayText=L_Reply_Text;
		if(itemTable.getAttribute("Ordering").length>=504)
		{
			var L_ReplyLimitMsg_Text="Cannot reply to this thread. The reply limit has been reached.";
			strAction="alert('"+L_ReplyLimitMsg_Text+"')";
		}
		else
		{
			strAction="STSNavigate('"+ctx.newFormUrl
+"&Threading="+escapeProperly(itemTable.getAttribute("Ordering"))
+"&Guid="+escapeProperly(itemTable.getAttribute("ThreadID"))
+"&Subject="+escapeProperly(itemTable.getAttribute("Subject"));
			strAction=AddSourceToUrl(strAction)+"')";
		}
		strImagePath=ctx.imagesPath+"reply.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 100);
		menuOption.id="ID_Reply";
	}
	AddSharedNamespaceMenuItems(m, ctx);
	var contentTypeId=itemTable.getAttribute("CId");
	if (contentTypeId !=null && contentTypeId.indexOf("0x0106")==0
			&& HasRights(0x10, 0x0))
	{
		strDisplayText=L_ExportContact_Text;
		strAction="STSNavigate('"+ctx.HttpPath+"&Cmd=Display&CacheControl=1&List="+ctx.listName+"&ID="+currentItemID+"&Using="+escapeProperly(ctx.listUrlDir)+"/vcard.vcf"+"')";
		strImagePath=ctx.imagesPath+"exptitem.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 350);
		CUIInfo(menuOption, "ExportContact", ["ExportContact"]);
		menuOption.id="ID_ExportContact";
	}
	CAMSep(m);
	if (ctx.verEnabled==1)
	{
		AddVersionsMenuItem(m, ctx, currentItemEscapedFileUrl);
	}
	/* Dont add the Approve/Reject ECB menu item
	if (ctx.isModerated==true &&
		HasRights(0x0, 0x10) && HasRights(0x0, 0x4) &&
		HasRights(0x0, 0x21000) && ctx.listBaseType !=4 &&
		currentItemID.indexOf(".0.") < 0)
	{
		strDisplayText=L_ModerateItem_Text;
		strAction="NavigateToApproveRejectAspx(event, '"+ctx.HttpRoot+"/_layouts/approve.aspx?List="+ctx.listName
+"&ID="+fixedItemId;
		strAction=AddSourceToUrl(strAction)+"')";
		strImagePath=ctx.imagesPath+"apprj.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 850);
		CUIInfo(menuOption, "Moderate", ["Moderate"]);
		menuOption.id="ID_ModerateItem";
	}
	CAMSep(m);
	*/
	AddWorkflowsMenuItem(m, ctx);
	var alertsEnabled=typeof(_spPageContextInfo) !='undefined' && _spPageContextInfo !=null && _spPageContextInfo.alertsEnabled;
	if ((currentItemID.indexOf(".0.") < 0)
		  && HasRights(0x80, 0x0)
		  && !ctx.ExternalDataList
		  && alertsEnabled)
	{
		strDisplayText=L_Subscribe_Text;
		strAction="NavigateToSubNewAspxV4(event, '"+ctx.HttpRoot+"', 'List="+ctx.listName+"&ID="+currentItemID+"')";
		strImagePath="";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1100);
		menuOption.id="ID_Subscribe";
		CUIInfo(menuOption, "Subscribe", ["Subscribe"]);
	}
	if (alertsEnabled || (ctx.WorkflowsAssociated && HasRights(0x0, 0x4)))
	{
		CAMSep(m);
	}
	AddManagePermsMenuItem(m, ctx, ctx.listName, currentItemID);
	if (currentItemID.indexOf(".0.") < 0 && HasRights(0x0, 0x8)
		  && !currentItemIsEventsExcp)
	{
		if (ctx.listBaseType==4)
			strDisplayText=L_DeleteResponse_Text;
		else
			strDisplayText=L_DeleteItem_Text;
		strAction="DeleteListItem()";
		strImagePath=ctx.imagesPath+"delitem.gif";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1180);
		CUIInfo(menuOption, "Delete", ["Delete"]);
		menuOption.id="ID_DeleteItem";
		CUIInfo(menuOption, "Delete", ["Delete"]);
	}
	var hasProgId=(currentItemProgId !=null) && (currentItemProgId !="");
	if (currentItemFSObjType==1 &&
		!hasProgId &&
		ctx.ContentTypesEnabled &&
		ctx.listTemplate !=108)
	{
		strDisplayText=L_CustomizeNewButton_Text;
		strAction="STSNavigate('"+ctx.HttpRoot+"/_layouts/ChangeContentTypeOrder.aspx?List="+ctx.listName+"&RootFolder="+currentItemEscapedFileUrl;
		strAction=AddSourceToUrl(strAction)+"')";
		strImagePath="";
		menuOption=CAMOpt(m, strDisplayText, strAction, strImagePath, null, 1170);
		CUIInfo(menuOption, "ChangeNewButton", ["ChangeNewButton"]);
		menuOption.id="ID_CustomizeNewButton";
	}
	return true;
}

SP.SOD.executeOrDelayUntilScriptLoaded(HideFeatureActivatedECBMenuItem, "SP.js");

When put together and activated via a Site scoped feature, the Approve/Reject menu item does not appear for the specific list, whether the list is viewed in a list view web part on a page, or in a list view page itself.

List View Web Parts on a Page.

Listview Web Part on a Page

Listview Web Part on a Page

In List View Pages.

List View Pages

List View Pages

Published by

Phil Harding

SharePoint Consultant, Developer, Father, Husband and Climber.

2 thoughts on “Hiding SharePoint ECB Menu Items for a Specific List

  1. Hi Phil. Can you advise if it is possible to implement this solution only using a CEWP and not linked to jquery, prototype, etc. Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.