Second Life of a Hungarian SharePoint Geek

August 29, 2016

Permission-based Rendering Templates, Part 2: The Synchronous Solution

Filed under: CSR, JavaScript, jQuery, REST, SP 2013 — Tags: , , , , — Peter Holpar @ 22:14

In my recent post I’ve illustrated how can you implement a permission-based custom rendering template using the JavaScript client object model (JSCOM)  and jQuery. That rendering template was implemented using the standard asynchronous JavaScript patterns via a callback method to not block the UI thread of the browser. In a fast network (in a LAN, for example) however, a synchronous implementation can function as well. Although there are some unsupported methods to make a JSCOM request synchronously, the JavaScript client object model was designed for asynchronous usage (see its executeQueryAsync method). To send our requests synchronously, we utilize the REST / OData interface in this post, and send the requests via the ajax function of jQuery.

To understand the original requirements and the configuration (field and list names, etc.), I suggest to read the first part first.

To enable using of jQuery selectors containing the dollar sign ($), we use the same escapeForJQuery helper function that we’ve created for the first part.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. var adminGroup = "MyGroup";
  9.  
  10. custom.escapeForJQuery = function (value) {
  11.     var newValue = value.replace(/\$/g, "\\$");
  12.     return newValue;
  13. }

Instead of simply wrapping the standard display template of choice fields (SPFieldChoice_Edit), the editFieldMethod function is responsible to get the HTML content of the field control, as it would be rendered without the customization by invoking the SPFieldChoice_Edit function, then we determine the group membership of the user by calling the synchronous isCurrentUserMemberOfGroup function (more about that a bit later), finally we alter the HTML content by hiding the adequate options by calling the hideOptions function (see it later as well).

  1. custom.editFieldMethod = function (ctx) {
  2.     var fieldSchema = ctx.CurrentFieldSchema;
  3.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  4.     var html = SPFieldChoice_Edit(ctx);
  5.  
  6.     var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  7.     if (isCurrentUserInGroup) {
  8.         html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  9.     }
  10.     else {
  11.         html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  12.     }
  13.  
  14.     return html;
  15. }

The hideOptions function loads the HTML source of the control into the DOM and removes the options that should be hidden for the given group. Finally it returns the HTML source of the altered control:

  1. custom.hideOptions = function (html, ctrlId, restrictedValues) {
  2.     var parsedHtml = $(html);
  3.     restrictedValues.forEach(function (rv) {
  4.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  5.         $(parsedHtml).find(selector).remove();
  6.     });
  7.     var result = $(parsedHtml).html();
  8.  
  9.     return result;
  10. }

The isCurrentUserMemberOfGroup function sends a synchronous REST request via the the ajax function of jQuery to determine the group membership of the current user:

  1. var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  2.  
  3. custom.isCurrentUserMemberOfGroup = function (groupName) {
  4.     var isMember = false;
  5.  
  6.     $.ajax({
  7.         url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  8.         type: "GET",
  9.         async: false,
  10.         contentType: "application/json;odata=verbose",
  11.         headers: {
  12.             "Accept": "application/json;odata=verbose",
  13.             "X-RequestDigest": $("#__REQUESTDIGEST").val()
  14.         },
  15.         complete: function (result) {
  16.             var response = JSON.parse(result.responseText);
  17.             if (response.error) {
  18.                 console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  19.             }
  20.             else {
  21.                 var groups = response.d.results;
  22.                 groups.forEach(function (group) {
  23.                     var loginName = group.LoginName;
  24.                     console.log(String.format("Group name: {0}", loginName));
  25.                     if (groupName == loginName) {
  26.                         isMember = true;
  27.                     }
  28.                 });
  29.             }
  30.         }
  31.     });
  32.  
  33.     return isMember;
  34. }

In this case we simply register the editFieldMethod for both the ‘EditForm’ and for the ‘NewForm’ mode of the Status field, there is no need for the OnPostRender method:

  1. var customOverrides = {};
  2. customOverrides.Templates = {};
  3.  
  4. customOverrides.Templates.Fields = {
  5.     'Status': {
  6.         'EditForm': custom.editFieldMethod,
  7.         'NewForm': custom.editFieldMethod
  8.     }
  9. };
  10.  
  11. SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);

The full source code of the rendering template introduced in this post:

  1. 'use strict';
  2.  
  3. (function () {
  4.  
  5.     var restrictedValues1 = ['Approved', 'Rejected'];
  6.     var restrictedValues2 = ['Resubmit'];
  7.  
  8.     var custom = custom || {};
  9.  
  10.     custom.controlId = null;
  11.  
  12.     var adminGroup = "MyGroup";
  13.  
  14.     custom.escapeForJQuery = function (value) {
  15.         var newValue = value.replace(/\$/g, "\\$");
  16.         return newValue;
  17.     }
  18.  
  19.     custom.hideOptions = function (html, ctrlId, restrictedValues) {
  20.         var parsedHtml = $(html);
  21.         restrictedValues.forEach(function (rv) {
  22.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  23.             $(parsedHtml).find(selector).remove();
  24.         });
  25.         var result = $(parsedHtml).html();
  26.  
  27.         return result;
  28.     }
  29.  
  30.     custom.editFieldMethod = function (ctx) {
  31.         var fieldSchema = ctx.CurrentFieldSchema;
  32.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  33.         var html = SPFieldChoice_Edit(ctx);
  34.  
  35.         var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  36.         if (isCurrentUserInGroup) {
  37.             html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  38.         }
  39.         else {
  40.             html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  41.         }
  42.  
  43.         return html;
  44.     }
  45.  
  46.     var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  47.  
  48.     custom.isCurrentUserMemberOfGroup = function (groupName) {
  49.         var isMember = false;
  50.  
  51.         $.ajax({
  52.             url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  53.             type: "GET",
  54.             async: false,
  55.             contentType: "application/json;odata=verbose",
  56.             headers: {
  57.                 "Accept": "application/json;odata=verbose",
  58.                 "X-RequestDigest": $("#__REQUESTDIGEST").val()
  59.             },
  60.             complete: function (result) {
  61.                 var response = JSON.parse(result.responseText);
  62.                 if (response.error) {
  63.                     console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  64.                 }
  65.                 else {
  66.                     var groups = response.d.results;
  67.                     groups.forEach(function (group) {
  68.                         var loginName = group.LoginName;
  69.                         console.log(String.format("Group name: {0}", loginName));
  70.                         if (groupName == loginName) {
  71.                             isMember = true;
  72.                         }
  73.                     });
  74.                 }
  75.             }
  76.         });
  77.  
  78.         return isMember;
  79.     }
  80.  
  81.     var customOverrides = {};
  82.     customOverrides.Templates = {};
  83.  
  84.     customOverrides.Templates.Fields = {
  85.         'Status': {
  86.             'EditForm': custom.editFieldMethod,
  87.             'NewForm': custom.editFieldMethod
  88.         }
  89.     };
  90.  
  91.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  92.  
  93. })();

Assuming your custom list is called PermBasedField, and both jQuery (in my case it is jquery-1.9.1.min.js) and our custom JavaScript rendering template (in my case it’s called permissionBasedFieldTemplate2.js) are stored in the root of the Site Assets library of the root web, you can register the template using the following PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["PermBasedField"]

$field = $list.Fields.GetFieldByInternalName("Status")
$field.JSLink = "~sitecollection/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate2.js"
$field.Update()

Note, that (in contrast to the script introduced in the first part of this post) there is no need for the JSCOM JavaScript files (sp.runtime.js and sp.js) in this case.

Advertisements

Permission-based Rendering Templates, Part 1: The Asynchronous Solution

Filed under: CSR, JavaScript, jQuery, JSCOM, SP 2013 — Tags: , , , , — Peter Holpar @ 21:49

Recently I read a question on SharePoint StackExchange about how one can restrict the available options in a choice field based on the group membership of the current user. My answer was to create a custom client rendering templates (CSR) and set it via the JSLink property of the choice field. If you are new in the usage of custom rendering templates, you can find several superb introduction on the topic on the web, like this one. At the time of my answer I had no sample code ready to publish (honestly, I was rather surprised that I have not found any such example on the web), but in the meantime I prepared two various implementations for the same problem. In this post I describe the first possible approach, an asynchronous solution based on the JavaScript client object model (JSCOM) and jQuery. The other solution will be discussed in a later post.

Both approaches share the same custom list: it is a list having a standard Title field and choice field called Status that has three state options: ‘Approved’, ‘Rejected’ and ‘Resubmit’. If the current use is member of a specific SharePoint group (let’s say ‘MyGroup’), the options ‘Approved’ and ‘Rejected’ should be displayed in the editable mode (that means on ‘EditForm’ and on ‘NewForm’), otherwise only the option ‘Resubmit’.

In our JavaScript rendering template we define the custom namespace, that includes the member properties and methods of the template. The same editFieldMethod function will be used in both editable modes. It’s simply a wrapper around the standard display template of choice fields (SPFieldChoice_Edit), the single extra work it performs is to store the ID of the corresponding HTML element (select in this case) into a member property called controlId. The standard format of the Id is NameOfTheChoiceField_GuidOfTheChoiceField__$DropDownChoice, for example in my case it is Status_fb5a9aac-5fdb-442e-96ac-ab7161cc4208_$DropDownChoice. We store its value to be able to find the HTML element and it children option elements via jQuery later in our asynchronous callback method.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. custom.editFieldMethod = function (ctx) {
  9.     var fieldSchema = ctx.CurrentFieldSchema;
  10.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  11.     var html = SPFieldChoice_Edit(ctx);
  12.     return html;
  13. }

We created a simple escapeForJQuery helper function to escape the dollar sign ($) in the ID, as I found IE 11 and  jQuery have issues with that character when used in selectors.

  1. custom.escapeForJQuery = function (value) {
  2.     var newValue = value.replace(/\$/g, "\\$");
  3.     return newValue;
  4. }

Note: you might have problems with the underscore (_) as well, especially if you use old browser versions, however I have not experienced such problems.In this case you should extend the escapeForJQuery helper function. See this guide:

Given this fact, authors who write CSS often attempt to employ the underscore in a similar fashion when creating class and ID names. This should not be done. Although underscores are, as of this writing, technically permitted in class and ID names, there are many historical and practical reasons why they should be avoided.

We utilize the escapeForJQuery function in our next helper function. The hideOptions method hides those options of a specific HTML element with ID specified in the ctrlId parameter that have any of the the values specified in the restrictedValues array parameter:

  1. custom.hideOptions = function (ctrlId, restrictedValues) {
  2.     restrictedValues.forEach(function (rv) {
  3.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  4.         $(selector).remove();
  5.     });        
  6. }

We use a third helper function called isCurrentUserMemberOfGroup to determine via CSOM if the current user is member of a group. This function – borrowed from this answer – has two parameters: the name of the group (groupName) and a callback method (OnComplete).

  1. custom.isCurrentUserMemberOfGroup = function (groupName, OnComplete) {
  2.  
  3.     var clientContext = new SP.ClientContext.get_current();
  4.     var currentUser = clientContext.get_web().get_currentUser();
  5.  
  6.     var userGroups = currentUser.get_groups();
  7.     clientContext.load(userGroups);
  8.  
  9.     clientContext.executeQueryAsync(OnSuccess, OnFailure);
  10.  
  11.     function OnSuccess(sender, args) {
  12.         var isMember = false;
  13.         var groupsEnumerator = userGroups.getEnumerator();
  14.         while (groupsEnumerator.moveNext()) {
  15.             var group = groupsEnumerator.get_current();
  16.             if (group.get_title() == groupName) {
  17.                 isMember = true;
  18.                 break;
  19.             }
  20.         }
  21.  
  22.         OnComplete(isMember);
  23.     }
  24.  
  25.     function OnFailure(sender, args) {
  26.         OnComplete(false);
  27.     }
  28. }

The isCurrentUserMemberOfGroup function is invoked by the applyPermissions function. In the callback function we hide the adequate options based on the group membership of the user.

  1. var adminGroup = "MyGroup";
  2.  
  3. custom.applyPermissions = function (ctx) {
  4.     custom.isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  5.         console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  6.  
  7.         if (custom.controlId) {
  8.             if (isCurrentUserInGroup) {
  9.                 custom.hideOptions(custom.controlId, restrictedValues1);
  10.             }
  11.             else {
  12.                 custom.hideOptions(custom.controlId, restrictedValues2);
  13.             }
  14.         }
  15.     });
  16. };

In our rendering template we register the custom editing method editFieldMethod, and set the applyPermissions function to be called as OnPostRender:

  1. var customOverrides = {};
  2. customOverrides.Templates = {};
  3.  
  4. customOverrides.Templates.Fields = {
  5.     'Status': {
  6.         'EditForm': custom.editFieldMethod,
  7.         'NewForm': custom.editFieldMethod
  8.     }
  9. };
  10.  
  11. customOverrides.Templates.OnPostRender = custom.applyPermissions;
  12.  
  13. SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);

The full source code of the rendering template introduced in this post:

  1. 'use strict';
  2.  
  3. (function () {
  4.  
  5.     var restrictedValues1 = ['Approved', 'Rejected'];
  6.     var restrictedValues2 = ['Resubmit'];
  7.  
  8.     var custom = custom || {};
  9.  
  10.     custom.controlId = null;
  11.  
  12.     custom.editFieldMethod = function (ctx) {
  13.         var fieldSchema = ctx.CurrentFieldSchema;
  14.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  15.         var html = SPFieldChoice_Edit(ctx);
  16.         return html;
  17.     }
  18.  
  19.     custom.isCurrentUserMemberOfGroup = function (groupName, OnComplete) {
  20.  
  21.         var clientContext = new SP.ClientContext.get_current();
  22.         var currentUser = clientContext.get_web().get_currentUser();
  23.  
  24.         var userGroups = currentUser.get_groups();
  25.         clientContext.load(userGroups);
  26.  
  27.         clientContext.executeQueryAsync(OnSuccess, OnFailure);
  28.  
  29.         function OnSuccess(sender, args) {
  30.             var isMember = false;
  31.             var groupsEnumerator = userGroups.getEnumerator();
  32.             while (groupsEnumerator.moveNext()) {
  33.                 var group = groupsEnumerator.get_current();
  34.                 if (group.get_title() == groupName) {
  35.                     isMember = true;
  36.                     break;
  37.                 }
  38.             }
  39.  
  40.             OnComplete(isMember);
  41.         }
  42.  
  43.         function OnFailure(sender, args) {
  44.             OnComplete(false);
  45.         }
  46.     }
  47.  
  48.     custom.escapeForJQuery = function (value) {
  49.         var newValue = value.replace(/\$/g, "\\$");
  50.         return newValue;
  51.     }
  52.  
  53.     custom.hideOptions = function (ctrlId, restrictedValues) {
  54.         restrictedValues.forEach(function (rv) {
  55.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  56.             $(selector).remove();
  57.         });        
  58.     }
  59.  
  60.     var adminGroup = "MyGroup";
  61.  
  62.     custom.applyPermissions = function (ctx) {
  63.         custom.isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  64.             console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  65.  
  66.             if (custom.controlId) {
  67.                 if (isCurrentUserInGroup) {
  68.                     custom.hideOptions(custom.controlId, restrictedValues1);
  69.                 }
  70.                 else {
  71.                     custom.hideOptions(custom.controlId, restrictedValues2);
  72.                 }
  73.             }
  74.         });
  75.     };
  76.     
  77.     var customOverrides = {};
  78.     customOverrides.Templates = {};
  79.  
  80.     customOverrides.Templates.Fields = {
  81.         'Status': {
  82.             'EditForm': custom.editFieldMethod,
  83.             'NewForm': custom.editFieldMethod
  84.         }
  85.     };
  86.  
  87.     customOverrides.Templates.OnPostRender = custom.applyPermissions;
  88.  
  89.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  90.     
  91. })();

Assuming your custom list is called PermBasedField, and both jQuery (in my case it is jquery-1.9.1.min.js) and our custom JavaScript rendering template (in my case it’s called permissionBasedFieldTemplate.js) are stored in the root of the Site Assets library of the root web, you can register the template using the following PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["PermBasedField"]

$field = $list.Fields.GetFieldByInternalName("Status")
$field.JSLink = "~sitecollection/_layouts/15/sp.runtime.js|~sitecollection/_layouts/15/sp.js|~sitecollection/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate.js"
$field.Update()

Stay tuned, the second part of the post including a synchronous approach should come soon.

July 26, 2016

Displaying Enterprise Custom Fields Conditionally on the Project Details Page of Project Server

In my recent blog entry I’ve illustrated how to change the out-of –the-box display order of the enterprise custom fields in the “Details” web part on the Project Details page of Project Server. In this post I go one step further and show, how to display / hide the fields based on conditions. As in the previous case, I use jQuery in this case either to achieve the goal.

For the sake of example, let’s assume you have a few custom fields (in this case “Field1” and “Field2”) that should be displayed only for the members of a special SharePoint group called “Admins”.

The script we need in this case is displayed in the code snippet below:

  1. var adminGroup = "Admins";
  2.  
  3. function isCurrentUserMemberOfGroup(groupName, OnComplete) {
  4.  
  5.     var clientContext = new SP.ClientContext.get_current();
  6.     var currentUser = clientContext.get_web().get_currentUser();
  7.  
  8.     var userGroups = currentUser.get_groups();
  9.     clientContext.load(userGroups);
  10.  
  11.     clientContext.executeQueryAsync(OnSuccess, OnFailure);
  12.  
  13.     function OnSuccess(sender, args) {
  14.         var isMember = false;
  15.         var groupsEnumerator = userGroups.getEnumerator();
  16.         while (groupsEnumerator.moveNext()) {
  17.             var group = groupsEnumerator.get_current();
  18.             if (group.get_title() == groupName) {
  19.                 isMember = true;
  20.                 break;
  21.             }
  22.         }
  23.  
  24.         OnComplete(isMember);
  25.     }
  26.  
  27.     function OnFailure(sender, args) {
  28.         OnComplete(false);
  29.     }
  30. }
  31.  
  32. $(document).ready(startScript);
  33.  
  34. function setFieldVisibility(fields, visible) {
  35.     // hide status / twitter related fields
  36.     $(".ms-accentText").each(function (index) {
  37.         if ($.inArray($(this).text(), fields) != -1) {
  38.             $(this).closest('tr').toggle(visible);
  39.         }
  40.     });
  41. }
  42.  
  43. function startScript() {
  44.     var fieldsToHide = ["Field1", "Field2"];
  45.  
  46.     // hide the fields at page startup
  47.     setFieldVisibility(fieldsToHide, false);
  48.     isCurrentUserMemberOfGroup(adminGroup, function (isCurrentUserInGroup) {
  49.         console.log("Current user is member of group '" + adminGroup + "': " + isCurrentUserInGroup);
  50.         setFieldVisibility(fieldsToHide, isCurrentUserInGroup);
  51.     });
  52. }

We use the setFieldVisibility function to hide / display the fields. On the page load we hide the fields in the first step. To verify if the current user belongs to the group and to perform a custom action after the check I use the isCurrentUserMemberOfGroup function borrowed from Stack Overflow. Finally, we set the visibility of the field in the callback function based on the group membership of the current user.

Assuming this script is saved in the file DisplayFieldsForGroup.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to the Project Details page, and include these two lines in the web part to inject the functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/emDisplayFieldsForGroup/em.js

Setting the Display Order of the Enterprise Custom Fields on the Project Details Page of Project Server

The out-of-the-box version of the Project Details page of Project Server contains two web parts. On the top, there is a web part with the title “Basic Info” (implemented in class Microsoft.Office.Project.PWA.WebParts.ProjectFieldPart, assembly Microsoft.Office.Project.Server.PWA) that displays the core information about the project, like its name and description as well as start and finish date and the name of the project owner.

There is another web part at the bottom of the page. It is the “Details” web part (implemented in class Microsoft.Office.Project.PWA.WebParts.ProjectCustomFieldsPart, assembly Microsoft.Office.Project.Server.PWA), and it displays the enterprise custom fields defined for the Project entity type.

In the “Basic Info” web part you can select, which project fields should be displayed, as well as their order via the web part properties, as displayed on the screenshot below:

image

image

However you don’t have such control on the fields displayed by the “Details” web part, it simply displays all of the project-related enterprise custom fields.

In this post I show you how to control the display order of the fields in the “Details” web part via jQuery, illustrating the possibilities by using two real-world problem as example.

Problem 1

Assume you have defined (among others) the following custom fields for the Project entity:

  • AField
  • AnotherField
  • CompanyComment
  • CompanyName
  • DivisionComment
  • DivisionName
  • JustOneMoreField
  • LastField

What can you do, if you have a business requirement that dictates the following order for these fields?

  • AField
  • AnotherField
  • DivisionName
  • DivisionComment
  • CompanyName
  • CompanyComment
  • JustOneMoreField
  • LastField

The fields highlighted with bold should be in the specified order. The other enterprise fields (including the ones created later) will be displayed before or after the highlighted block and sorted alphabetically.

You have the option to remove the “Details” web part from the page, and include all of the fields you need in the “Basic Info” web part in the required order. This solution seems to be simple at first sight, but it has the drawback, that you lose the original page structure with the two web parts, and what even worse, you should add any new enterprise custom field to the “Basic Info” web part manually in the future.

Alternatively, you can use jQuery to achieve the required results within the “Details” web part.

The changeFieldOrder function below demonstrates how to accomplish this goal. This function invokes the getTRForField function to find the table row for the specified field. It looks up then the row that belongs to the field specified in the first member of the string array (fieldOrder) it receives as parameter, then looks up the other rows one after the other as well, and places them after the previous one.

Optionally, you can define further field order sets and call the changeFieldOrder function repeatedly using these field sets as parameter.

  1. $(document).ready(startScript);
  2.  
  3. function getTRForField(parent, fieldName) {
  4.     var result = parent.find("h3.ms-accentText").filter(function () { return $(this).text() === fieldName; }).closest("tr").first();
  5.     return result;
  6. }
  7.  
  8. function changeFieldOrder(fieldOrder) {
  9.     var wp = $("div[name='WPClass:ProjectFieldPart']").last();
  10.     var tbody = wp.find("tbody").first();
  11.     for (i = 1; i < fieldOrder.length; i++) {
  12.         var firstTR = getTRForField(tbody, fieldOrder[i – 1]);
  13.         var secondTR = getTRForField(tbody, fieldOrder[i]);
  14.         firstTR.after(secondTR);
  15.     }
  16. }
  17.  
  18. function startScript() {
  19.  
  20.     var fieldOrder1 = ["DivisionName", "DivisionComment", "CompanyName", "CompanyComment"];
  21.     // you can define further field orders if you need
  22.     // var fieldOrder2 = ["CustomField2", "CustomField1"];
  23.  
  24.     changeFieldOrder(fieldOrder1);
  25.     // changeFieldOrder(fieldOrder2);
  26.  
  27. }

Assuming this script is saved in the file CustomFieldOrder.js in the Site Assets library of the PWA site, and the jquery-1.9.1.min.js can be found in that library as well, you can add a Script Editor web part to the Project Details page, and include these two lines in the web part to inject the field-sorting functionality into the page:

/PWA/SiteAssets/jquery-1.9.1.min.js
/PWA/SiteAssets/CustomFieldOrder.js

Problem 2

Assume you would like to display the fields in this order:

  • DivisionName
  • DivisionComment
  • CompanyName
  • CompanyComment
  • AField
  • AnotherField
  • JustOneMoreField
  • LastField

The fields highlighted with bold should be at the very top of the field list and in the specified order. The other enterprise fields (including the ones created later) will be displayed after the highlighted block and sorted alphabetically.

Again, you have the option to remove the “Details” web part from the page, and include all of the fields you need in the “Basic Info” web part in the required order, but in this case you have the same drawbacks, as in the first case above.

Instead of this, you can use the setFieldOrder function that invokes the same getTRForField function we saw in the example above. Then it looks up the row that belongs to the field specified in the first member of the string array (fieldOrder) it receives as parameter, put it at the first row of the table, then looks up the other rows one after the other as well, and places them after the previous one.

  1. function getTRForField(parent, fieldName) {
  2.     var result = parent.find("h3.ms-accentText").filter(function () { return $(this).text() === fieldName; }).closest("tr").first();
  3.     return result;
  4. }
  5.  
  6. function setFieldOrder(fieldOrder) {
  7.     var wp = $("div[name='WPClass:ProjectFieldPart']").last();
  8.     var tbody = wp.find("tbody").first();
  9.     var trLast;
  10.     for (i = 0; i < fieldOrder.length; i++) {
  11.         var tr = getTRForField(tbody, fieldOrder[i]);
  12.         if (!trLast) {
  13.             tr.prependTo(tbody);
  14.             trLast = tr;
  15.         }
  16.         else {
  17.             trLast.after(tr);
  18.             trLast = tr;
  19.         }
  20.     }
  21. }
  22.  
  23. function startScript() {
  24.     var fieldOrder = ["DivisionName", "DivisionComment", "CompanyName", "CompanyComment"];
  25.     setFieldOrder(fieldOrder);
  26. }

You can combine the usage of the changeFieldOrder and setFieldOrder functions if you wish.

In the next blog post I plan to stay with the same topic, how the enterprise custom fields get displayed in the “Details” web part, but instead of setting display order, I show you how to hide / display them based on conditions, like the group membership of the current user.

July 22, 2014

How to validate file names of attachments on SharePoint forms

Filed under: Attachments, jQuery, SP 2010 — Tags: , , — Peter Holpar @ 18:36

A few months ago I already posted a solution to check duplicated attachment names on SharePoint forms. Another common problem source are invalid file names.

For example, if you have a standard list item edit form, and would like to upload an attachment with invalid file name (for example, invalid characters or exceeding length limitation), on submitting the form you receive an exception. Despite of the error, the item itself is saved, however, if you appended other files (with valid names) as well, the ones you appended after the attachment having invalid name are not saved to the item.

The error message you receive if the file name contains invalid characters:

The file or folder name contains characters that are not permitted.  Please use a different name.<nativehr>0x81020073</nativehr><nativestack></nativestack>

The stack trace:

[COMException (0x81020073): The file or folder name contains characters that are not permitted.  Please use a different name.<nativehr>0x81020073</nativehr><nativestack></nativestack>]
   Microsoft.SharePoint.Library.SPRequestInternalClass.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback) +0
   Microsoft.SharePoint.Library.SPRequest.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback) +406

[SPException: The file or folder name contains characters that are not permitted.  Please use a different name.]
   Microsoft.SharePoint.SPGlobal.HandleComException(COMException comEx) +27609826
   Microsoft.SharePoint.Library.SPRequest.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback) +28003823
   Microsoft.SharePoint.SPListItem.AddOrUpdateItem(Boolean bAdd, Boolean bSystem, Boolean bPreserveItemVersion, Boolean bNoVersion, Boolean bMigration, Boolean bPublish, Boolean bCheckOut, Boolean bCheckin, Guid newGuidOnAdd, Int32& ulID, Object& objAttachmentNames, Object& objAttachmentContents, Boolean suppressAfterEvents, String filename) +26729805
   Microsoft.SharePoint.SPListItem.UpdateInternal(Boolean bSystem, Boolean bPreserveItemVersion, Guid newGuidOnAdd, Boolean bMigration, Boolean bPublish, Boolean bNoVersion, Boolean bCheckOut, Boolean bCheckin, Boolean suppressAfterEvents, String filename) +26726414
   Microsoft.SharePoint.SPListItem.Update() +161
   Microsoft.SharePoint.WebControls.SaveButton.SaveItem(SPContext itemContext, Boolean uploadMode, String checkInComment) +848
   Microsoft.SharePoint.WebControls.SaveButton.OnBubbleEvent(Object source, EventArgs e) +1315
   System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args) +70
   System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) +29
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +2981

image

The error message you receive if the file name is too long:

The specified file or folder name is too long. The URL path for all files and folders must be 260 characters or less (and no more than 128 characters for any single file or folder name in the URL). Please type a shorter file or folder name.<nativehr>0x800700ce</nativehr><nativestack></nativestack>

image

The issue is even more annoying if you have an edit form with some kind of custom logic behind it. For example, in one of our applications, there is a custom approval workflow attached to the list that is started automatically after the item is saved. If the file name is invalid, the workflow starts without the (probably for the approval important) attachment. It doesn’t really help customer satisfaction, I shouldn’t say.

How could we validate attachment file names and prohibit attachments having invalid names? We can use the same method as described in my former post. We should update our onAttachOKbuttonClicked method with the file name validations. The new version, including the duplicate check as well as file name length and special character validation:

  1. $(document).ready(attachEventHandlers);
  2.  
  3. function attachEventHandlers() {
  4.     // override the default event handler with our custom method
  5.     $('#attachOKbutton').attr("onclick", "onAttachOKbuttonClicked()");
  6. }
  7.  
  8. function onAttachOKbuttonClicked() {
  9.     var newFilePath = $('#attachmentsOnClient').find('input').last().val();
  10.     // get the file name from the file path as described at
  11.     // http://stackoverflow.com/questions/423376/how-to-get-the-file-name-from-a-full-path-using-javascript
  12.     // TrimWhiteSpaces is a js method of SharePoint to filter out special characters from the file name
  13.     var newFileName = TrimWhiteSpaces(newFilePath).replace(/^.*[\\\/]/, '');
  14.  
  15.     var maxFileNameLength = 128;
  16.  
  17.     // Information about the characters that you cannot use in site names, folder names, and file names in SharePoint
  18.     // http://support.microsoft.com/kb/905231
  19.     // http://www.sysadminsblog.com/microsoft/file-name-length-and-character-restrictions-for-sharepoint/
  20.     // ivalid characters in SP:
  21.     // ~#%&*{}\:<>?/+|"
  22.     // ivalid characters in file system:
  23.     // \/:*?"<>|
  24.     // we have to check only:
  25.     // ~#%&{}+|
  26.     /*
  27.     Cant be longer than 128 characters
  28.     Cant use: ~ # % & * { } \ : < > ? / + | "; RegExp: [~#%\&{}+\|] – do not include characters that are not allowed in the file system
  29.     Cant use the period character consecutively in the middle of a file name (blahblah.docx); RegExp: \\.\\.
  30.     Cant use the period character at the end of a file name; RegExp:  ^\\.
  31.     Cant use the period character at the start of a file name; RegExp:  \\.$    
  32.     */
  33.  
  34.     var match = (new RegExp('[~#%\&{}+\|]|\\.\\.|^\\.|\\.$')).test(newFileName);
  35.     if (match) {
  36.         alert("Ivalid file name. The name of the attached file contains invalid characters.");
  37.     }
  38.     else if (newFileName.length > maxFileNameLength) {
  39.         alert("Ivalid file name. The name of the attached file is too long.");
  40.     }
  41.     else {
  42.         // it is the same duplicate check code from former post (https://pholpar.wordpress.com/2014/03/12/how-to-check-for-duplicated-attachments-on-sharepoint-forms/)
  43.         var foundDuplication = false;
  44.  
  45.         $('#idAttachmentsTable').find('tbody').find('tr').each(function () {
  46.             var existingFileName = $(this).find('.ms-vb').find('a').text();
  47.             // if the existingFileName is empty then the attachment was uploaded in this session
  48.             // that is, it is not saved yet
  49.             if (existingFileName == '') {
  50.                 var existingFilePath = $(this).find('.ms-vb').find('span').text();
  51.                 existingFileName = existingFilePath.replace(/^.*[\\\/]/, '');
  52.             }
  53.  
  54.             if (newFileName == existingFileName) {
  55.                 foundDuplication = true;
  56.                 return false;
  57.             }
  58.         });
  59.  
  60.         if (foundDuplication) {
  61.             alert("A file with name '" + newFileName + "' is already attached to this item.");
  62.         }
  63.         else {
  64.             // call the OkAttach js method of SharePoint
  65.             // this is the method that is originally called by uploading attachments
  66.             OkAttach();
  67.         }
  68.  
  69.     }
  70. }

Note: I check only special characters that are supported in the NTFS file system, but not supported in SharePoint file names. I don’t check if the file name contains officially forbidden strings (.files, –Dateien, etc.). Reason is that up to now I had no issues with such files.

Including this method on the form helps us to prevent submitting the form with invalid attachments, and let the user to fix potential issues around attachment file names before saving the item.

March 12, 2014

How to check for Duplicated Attachments on SharePoint Forms

Filed under: Attachments, jQuery, SP 2010 — Tags: , , — Peter Holpar @ 23:27

Assume you have a SharePoint form with a lot of input fields as well as the option to append attachments to the list item. Assume one of your users accidentally tries to attach the same file twice (or two separate files with the same name) to the list item. When submitting the form, the following exception is displayed:

Failed to get value of the "Attachments" column from the "Attachments" field type control.  See details in log. Exception message: A file with the name already exists.

image

Note: the reason behind this error is that attachments in SharePoint are stored in individual folders by the item Id (for example, attachments of item with Id 34 of list YourList are stored in folder /lists/YourList/Attachments/34) and you cannot store multiple files within the same folder. This is different as the attachment handling with mails, where you can attach multiple files with the same name without any errors.

You can imagine the frustration of the user, as all of the data typed into the form was lost. Wouldn’t it be better not to allow the user to choose a file to attach if a file with the same name was already attached to the item? Definitely it would be far better, and it is quite simply to fulfill using jQuery.

To be able to perform the validation, first we have to understand, how the name of the attachments are store on the page. Assume a file called test1.txt is already attached to the item, and in this editing session we chose the file called test2.txt (from the path c:\temp) to be attached (as display below).

image

In this case, the HTML DOM of the page looks like this:

image

The key information: a table element with id idAttachmentsTable contains a tbody element, that has tr elements including td elements with class ms-vb. If the file is already attached to the item, then the file name is included in a child a element. If the file has been just uploaded, a span element includes the full path and the file name.

The next task is to find out how we can get the name of the file we would like to attach.

image

Analyzing the HTML of the page we can found an input element of type file under the td element with id attachmentsOnClient:

image

So first I tried to use the value of the onetidIOFile input field, but it turned out that the value of this field does not change as we attach further files to the item.

Assuming we upload the files file1.docx, file2.xlsx and file3.png in this sequence, the value of the $("#attachmentsOnClient").html() expression changes as described below:

Step 1. attaching file1.docx:
<SPAN dir=ltr><INPUT name=fileupload0 title="Name  " class=ms-fileinput id=onetidIOFile type=file size=56 value=C:\directory\file1.docx></INPUT> </SPAN>

Step 2. attaching file2.xlsx:
<SPAN dir=ltr><INPUT name=fileupload0 title="Name  " class=ms-fileinput id=onetidIOFile style="DISPLAY: none" type=file size=56 value=C:\directory\file1.docx></INPUT> </SPAN><INPUT name=fileupload1 tabIndex=1 title=Name class=ms-longfileinput id=fileupload1 type=file size=56 value=C:\directory\file2.xlsx>

Step 3. attaching file3.png:
<SPAN dir=ltr><INPUT name=fileupload0 title="Name  " class=ms-fileinput id=onetidIOFile style="DISPLAY: none" type=file size=56 value=C:\directory\file1.docx></INPUT> </SPAN><INPUT name=fileupload1 class=ms-longfileinput style="DISPLAY: none" type=file value=C:\directory\file2.xlsx><INPUT name=fileupload2 tabIndex=1 title=Name class=ms-longfileinput id=fileupload2 type=file size=56 value=C:\directory\file3.png>

We can see, that former input fields of type file got hidden via style="DISPLAY: none", and new file input elements are appended to the existing ones, That means, we can get the path of the actually attached file from the last file input element.

Last action is to replace the default event handler method (OkAttach) on the OK button. In the new event handler we get the name of the file being actually appended and compare it with the file names of other attachments (attached either in this editing session or already saved to the item). If we find a file with the same name, then a warning is displayed. If there is no conflicting attachment, then we call the default event handler that registers the file to be uploaded as attachment.

  1. $(document).ready(attachEventHandlers);
  2.  
  3. function attachEventHandlers() {
  4.   // override the default event handler with our custom method
  5.   $('#attachOKbutton').attr("onclick", "onAttachOKbuttonClicked()");
  6. }
  7.  
  8. function onAttachOKbuttonClicked() {
  9.   // get the name of the file last attached to the item
  10.   var newFilePath = $('#attachmentsOnClient').find('input').last().val();
  11.   // get the file name from the file path as described at
  12.   // http://stackoverflow.com/questions/423376/how-to-get-the-file-name-from-a-full-path-using-javascript
  13.   // TrimWhiteSpaces is a js method of SharePoint to filter out special characters from the file name
  14.   var newFileName = TrimWhiteSpaces(newFilePath).replace(/^.*[\\\/]/, '');
  15.  
  16.   var foundDuplication = false;
  17.  
  18.   $('#idAttachmentsTable').find('tbody').find('tr').each(function () {
  19.     var existingFileName = $(this).find('.ms-vb').find('a').text();
  20.     // if the existingFileName is empty then the attachment was uploaded in this session
  21.     // that is, it is not saved yet
  22.     if (existingFileName == '') {
  23.       var existingFilePath = $(this).find('.ms-vb').find('span').text();
  24.       existingFileName = existingFilePath.replace(/^.*[\\\/]/, '');
  25.     }
  26.  
  27.     if (newFileName == existingFileName) {
  28.       foundDuplication = true;
  29.       return false;
  30.     }
  31.   });
  32.  
  33.   if (foundDuplication) {
  34.       alert("A file with name '" + newFileName + "' is already attached to this item.");
  35.   }
  36.   else {
  37.     // call the OkAttach js method of SharePoint
  38.     // this is the method that is originally called by uploading attachments
  39.     OkAttach();
  40.   }
  41. }

Hopefully you can apply this method to make the user experience better (or at least less frustrating) when working with attachments.

March 6, 2014

How to enable Users to Close Status Messages and Notifications

Filed under: jQuery, SP 2010 — Tags: , — Peter Holpar @ 22:46

In SharePoint you can give feedback to your users through status messages and notifications. The notifications can disappear automatically (if you call the addNotification method with false as the second parameter), and we can hide status messages as well from code using JavaScript setTimeout method (see examples here). Assuming that your scripts are running in the background and can give feedback not only as a direct response to a user action, it is not ideal, if your messages disappear after a few seconds, as the user may omit them easily. On the other side, it would not be nice, if the messages flooded the UI, without the possibility the user can close them after reading, similarly to the OK button in a JavaScript alert.

Fortunately, it’s quite easy to inject this feature using jQuery. For example, we can insert an x into the status message using the code below. The message disappears as the user clicks on the x:

var statusId = SP.UI.Status.addStatus("Status message:", "This is a test message");
makeStatusClosable(statusId);

function makeStatusClosable(statusId) {
    var status = $(‘#’ + statusId);
    $(status).html(‘<a title="Close status message" href="" onclick="javascript:SP.UI.Status.removeStatus(\” + statusId + ‘\’);javascript:return false;">x</a>&nbsp;’ + $(status).html());
}

The result is illustrated on the following screenshot:

image

The same solution for the notifications looks like this:

var notificationId = SP.UI.Notify.addNotification("This is a test message", true);
makeNotificationClosable(notificationId);

function makeNotificationClosable(notificationId) {
    var notification = $(‘#’ + notificationId).find(‘.s4-noti-in3’);
    $(notification).html(‘<a title="Close notification" href="" onclick="javascript:SP.UI.Notify.removeNotification(\” + notificationId + ‘\’);javascript:return false;">x</a>&nbsp;’ + $(notification).html());
}

And the outcome is:

image

March 5, 2014

February 19, 2014

Fault-tolerant REST Queries

Filed under: jQuery, REST, SP 2010, SP 2013 — Tags: , , , — Peter Holpar @ 22:15

Recently I faced again with the problem, that as the scripts on my web pages submit REST queries after a longer pause or an IISRESET (that is, when the IIS application pools must be started), I receive on of the following errors on the first try:

‘Microsoft.SharePoint.DataService.ListNameItem’ does not have a property named ‘FieldName’. (SharePoint 2010, HTTP 400 – Bad Request in the browser)
An error occurred while processing this request.  (SharePoint 2010, HTTP 500 – Internal Server Error in the browser)
No property ‘FieldName’ exists in type ‘Microsoft.SharePoint.Linq.DataServiceEntity’ at position 0. (SharePoint 2013, HTTP 400 – Bad Request in the browser)

When one submits the query via the browser, only the HTTP 400 / HTTP 500 error codes are displayed. You can see the detailed error message only via a network traffic analyzer tool, like Fiddler.

The subsequent queries (even using the same expression as originally) usually succeed. If we submits multiple queries at the same time (for example via a script on a page), it might happen that several of these queries fail with the same error.

How to avoid this kind of failures in a web application, when it is especially important to prevent the erratic behavior?

Using jQuery we can create a function like the one below to re-submit the query automatically and silently in case of errors. I applied a time-gap of 1 second in the sample to give the system time to wake up and a maximum number of 3 retries. If even after the 3rd retry we encounter an error, we simply display it to the user.

// alter the value to match your site
var baseUrl = ‘http://yoursharepoint/yoursite/_vti_bin/listdata.svc/’;

function sendRESTQuery(queryUrl, onSuccess, retryCount) {
    var retryInterval = 1000; // 1 sec.
    var retryCountMax = 3;
    // use a default value of 0 if no value for retryCount passed
    var retryCount = (retryCount != undefined) ? retryCount : 0;

    $.ajax({
        type: ‘GET’,
        contentType: ‘application/json;odata=verbose’,
        url: baseUrl + queryUrl,
        headers: {
            ‘X-RequestDigest’: $(‘#__REQUESTDIGEST’).val(),
            "Accept": "application/json; odata=verbose"
        },
        dataType: "json",
        complete: function (result) {               
            var response = JSON.parse(result.responseText);
            if (response.error) {
                // here you can include your custom logic to differentiate for example based on the error types
                if (retryCount <= retryCountMax) {
                    window.setTimeout(function () { sendRESTQuery(queryUrl, onSuccess, retryCount++) }, retryInterval);
                }
                else {
                    alert("Error: " + response.error.code + "\n" + response.error.message.value);
                }
            }
            else {
                bgDetails = response.d;
                onSuccess(bgDetails);
            }
        }
    });      
}

Assuming we have a HTML page with a SELECT element having id = “dropItemList”, and a SharePoint list called YourList, the following code snippet demonstrates, how to fill up the options of the SELECT with the list items:

var dropItemListSelector = "#dropItemList";

function initDropItemList() {
    $(dropItemListSelector).hide();
    var queryUrl = "YourList()?$select=Id,Title";
    sendRESTQuery(queryUrl, function (responseData) {
        var items = responseData.results;
        $(dropItemListSelector).find(‘option’).remove().end().append(‘<option value="0">Please choose an item!</option>’);
        Enumerable.from(items).orderBy("$.Title").forEach(function (x) {
            $(‘<option>’).val(x.Id).text(x.Title).appendTo(dropItemListSelector);
        });
        $(dropItemListSelector).show();
    });       
}

In the previous example I used the linq.js ver.3.0.4-Beta5 to assemble my LINQ query. Version of the library is important!

August 22, 2013

Autocomplete textbox for site groups

Filed under: CEWP, jQuery, jQuery UI, SP 2010, SPServices — Tags: , , , , — Peter Holpar @ 22:44

Recently I had to provide a solution that supports selecting SharePoint site groups through an HTML autocomplete textbox.

The solution was built on JavaScript-based components, like jQuery (version 1.8.3), Autocomplete widget of jQuery UI (version 1.10.3) and jQuery Library SharePoint Web Services (aka SPServices, version 2013.01). The JavaScript code was injected into the page using the Content Editor Web Part (CEWP).

In the code (see below) we get the list of groups through the GetGroupCollectionFromSite method of the UserGroup web service, and attach the autocomplete feature to a HTML textbox, filtering the matching groups by a simple RegExp based on the text entered into the textbox.

  1. <script src="/_layouts/jQuery/jquery-1.8.3.min.js"></script>
  2. <script src="/_layouts/jQuery/jquery-ui-1.10.3.custom.min.js"></script>
  3. <script type="text/javascript" src="/_layouts/jQuery/jquery.SPServices-2013.01.min.js"></script>
  4. <link rel="stylesheet" type="text/css" href="/_layouts/jQuery/css/ui-lightness/jquery-ui-1.10.3.custom.min.css">
  5.  
  6. <input id="autocomplete">
  7.  
  8. <script language="ecmascript" type="text/ecmascript">
  9.  
  10. $(document).ready(startScript);
  11.  
  12. function startScript() {
  13.   $().SPServices({
  14.         operation: "GetGroupCollectionFromSite",
  15.         async: true,        
  16.         completefunc: AttachAutoComplete,
  17.     webURL: "/"
  18.     });
  19. }
  20.  
  21. function AttachAutoComplete(xmlResponse) {
  22.   var domElementArray = $.makeArray(xmlResponse.responseXML.getElementsByTagName("Group"));
  23.   var data = $.map(domElementArray , function(node) {
  24.         return node.getAttribute("Name");
  25.     });   
  26.  
  27.   $( "#autocomplete" ).autocomplete({
  28.     source: function( request, response ) {
  29.       var matcher = new RegExp( $.ui.autocomplete.escapeRegex( request.term ), "i" );
  30.       response( $.grep( data, function( item ) {
  31.         return matcher.test( item );
  32.       }) );
  33.     }
  34.   });     
  35. }
  36.  
  37. </script>

And here is the feature in action:

image

Older Posts »

Create a free website or blog at WordPress.com.