An alternative way of getting Client Object properties using SharePoint REST and CSOM APIs

When working with client APIs such as JSOM or REST you have probably noticed that certain properties of objects are not available compared to SSOM counterparts. For example, SPList class exposes SPList.Author property for getting an SPUser object that represents information about the user who created the list which in turn is not available for SP.List object. Hence the question arises, how those properties could be retrieved using client APIs?

The solution that I would like to demonstrate is based on retrieving client object properties from XML schema.

Getting SP.List object properties using JSOM

The function getListProperties is indented for loading list schema of the SP.List using SP.List.schemaXml property and extracting properties from it’s value:

function getListProperties(listTitle, OnSuccess,OnError) {
var context = SP.ClientContext.get_current();
var web = context.get_web();
var list = web.get_lists().getByTitle(listTitle);
context.load(web);
context.load(list,'SchemaXml');
context.executeQueryAsync(function(){
var schemaXml = list.get_schemaXml();
var listJson = schemaXml2Json(schemaXml);
OnSuccess(listJson);
},OnError);
}
function schemaXml2Json(schemaXml)
{
var jsonObject = {};
var schemaXmlDoc = $.parseXML(schemaXml);
$(schemaXmlDoc).find('List').each(function() {
$.each(this.attributes, function(i, attr){
jsonObject[attr.name] = attr.value;
});
});
return jsonObject;
}

view raw
getListProperties.js
hosted with ❤ by GitHub

The following example demonstrates how to retrieve Author property  using the specified method:

Example:

function getListAuthor(listTitle,OnSuccess,OnError) {
getListProperties(listTitle,
function(listJson){
var context = SP.ClientContext.get_current();
var listAuthor = context.get_web().getUserById(listJson.Author);
context.load(listAuthor);
context.executeQueryAsync(
function(){
OnSuccess(listAuthor);
},OnError);
},
OnError);
}
//Usage
getListAuthor('Discussion Board',
function(author){
console.log('List created by: ' + author.get_loginName());
},
function(sender,args){
console.log('Error occured:' + args.get_message());
}
);

view raw
getListAuthor.js
hosted with ❤ by GitHub

Getting SP.List object properties using REST

The following REST endpoint is used for retrieving SP.List.schemaXml property:

http://<sitecollection>/<site>/_api/web/lists/getbytitle(listtitle)/schemaXml

function getJson(url)
{
return $.ajax({
url: url,
type: "GET",
contentType: "application/json;odata=verbose",
headers: {
"Accept": "application/json;odata=verbose"
}
});
}
function getListProperties(listTitle) {
var listEndpointUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('" + listTitle + "')/SchemaXml";
return getJson(listEndpointUrl).then(
function(data){
var schemaXml = data.d.SchemaXml;
return schemaXml2Json(schemaXml);
});
}

view raw
getListProperties.js
hosted with ❤ by GitHub

Example:

The same example that demonstrates how to retrieve Author property of List resource using REST:

getListProperties('Discussion Board')
.done(function(properties)
{
console.log('List created by: ' + properties.Author);
})
.fail(
function(error){
console.log(JSON.stringify(error));
});

view raw
getListAuthor.js
hosted with ❤ by GitHub

Manage User Custom Actions in Office 365

Overview

Custom Actions offer a flexible way to extend capabilities of the SharePoint. The possibilities span the range of including custom JavaScript on every page to extending the Ribbon. In SharePoint 2013/SharePoint Online you can leverage the CSOM/REST  to manage custom actions. Below are demonstrated two simple examples of using custom actions in real world scenarios and I hope you you’ll find them useful.

Example 1. Enable jQuery

Let’s get started with an example that demonstrate how to add jQuery library to Office 365/SharePoint Online site. Unfortunately  it is not supported to reference external resources, for example from Microsoft Ajax Content Delivery Network (CDN) that hosts popular third party JavaScript libraries including jQuery. The prerequisite for referencing JavaScript files is that they could only be accesible when located within the site collection. So, the first step would be to save a jQuery library into Style Library: /Style Library/Scripts/jQuery/jquery-2.1.1.js.

The following Activate-JQuery.ps1 script  demonstrates how to enable jQuery library  in Office 365/SharePoint Online site

param(
[string]$SiteUrl,
[string]$UserName,
[string]$Password
)
. ".\UserCustomActions.ps1"
<#
.SYNOPSIS
Enable jQuery Library
.DESCRIPTION
Enable jQuery Library in Office 365/SharePoint Online site
.EXAMPLE
.\Activate-JQuery.ps1 -SiteUrl "https://tenant-public.sharepoint.com&quot; -UserName "username@tenant.onmicrosoft.com" -Password "password"
#>
Function Activate-JQuery([string]$SiteUrl,[string]$UserName,[string]$Password)
{
$context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$context.Credentials = Get-SPOCredentials UserName $UserName Password $Password
$sequenceNo = 1482
$jQueryUrl = "~SiteCollection/Style Library/Scripts/jQuery/jquery-2.1.1.js"
Add-ScriptLinkAction Context $Context ScriptSrc $jQueryUrl Sequence $sequenceNo
$context.Dispose()
}
ActivateJQuery SiteUrl $SiteUrl UserName $UserName Password $Password

view raw
Activate-JQuery.ps1
hosted with ❤ by GitHub

Dependencies: UserCustomActions.ps1

Example 2. Enable Google Analytics

The following Activate-GoogleAnalytics.ps1 script demonstrates how to activate tracking code in Office 365/SharePoint Online site

param(
[string]$SiteUrl,
[string]$UserName,
[string]$Password
)
. ".\UserCustomActions.ps1"
<#
.SYNOPSIS
Activate Google Analytics
.DESCRIPTION
Activate Google Analytics in Office 365/SharePoint Online site
.EXAMPLE
.\Activate-GoogleAnalytics.ps1 -SiteUrl "https://tenant-public.sharepoint.com&quot; -UserName "username@tenant.onmicrosoft.com" -Password "password"
#>
Function Activate-GoogleAnalytics([string]$SiteUrl,[string]$UserName,[string]$Password)
{
$context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$context.Credentials = Get-SPOCredentials UserName $UserName Password $Password
$sequenceNo = 1480
#Insert your Tracking code here or specify valid Tracking ID
$TrackingCode = "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXXXXX-X', 'auto');
ga('send', 'pageview');"
Add-ScriptLinkAction Context $Context ScriptBlock $TrackingCode Sequence $sequenceNo
$context.Dispose()
}
ActivateGoogleAnalytics SiteUrl $SiteUrl UserName $UserName Password $Password

Dependencies: UserCustomActions.ps1

Follow these instructions to use Google Analytics to collect data from Office 365/SharePoint Online sites.

To set up the web tracking code:

  1. Find the tracking code snippet for your property.
    Sign in to your Google Analytics account, and click Admin in the top menu bar. From the Account and Propertycolumns, select the property you’re working with. Click Tracking Info / Tracking Code.
    SNP_8C6378625835DB93E9293E76F24E9AC45177_3517951_en_v4
  2. Find your tracking code snippet. It’s in a box with several lines of JavaScript in it. Everything in this box is your tracking code snippet. It starts with <script> and ends with </script>.
    The tracking code contains a unique ID that corresponds to each Google Analytics property. Don’t mix up tracking code snippets from different properties, and don’t reuse the same tracking code snippet on multiple domains. Click to expand this image and see where the tracking code snippet is in the interface.
  3. Open , paste the tracking code into $TrackingCode variable and run the script to register tracking code in Office 365/SharePoint Online site
  4. Check your set up.
    Make sure that the tracking snippet installed on your website matches the code shown in the view, and see more ways you can verify your set up.

List Items manipulation via REST API in SharePoint 2010

Introduction

Apart from CSOM API,  REST API introduces another approach to access SharePoint list data from platforms on which the CSOM may be unavailable. The SharePoint REST interface is based on the REST-based Open Data protocol (OData) which  is a platform-independent open standard.

Examples

This section contains sample code for all of the CRUD operations.

Create

In order to  perform a Create operation via REST, you must perform the following actions:

  • Create an HTTP request using the POST verb.
  • Use the service URL of the list to which you want to add an entity as the target for the POST.
  • Set the content type to application/json.
  • Serialize the JSON objects that represent your new list items as a string, and add this value to the request body.

The following code snippet demonstrates how to perform a Create operation against a SharePoint list.

function createListItem(webUrl,listName, itemProperties, success, failure) {
$.ajax({
url: webUrl + "/_vti_bin/listdata.svc/" + listName,
type: "POST",
processData: false,
contentType: "application/json;odata=verbose",
data: JSON.stringify(itemProperties),
headers: {
"Accept": "application/json;odata=verbose"
},
success: function (data) {
success(data.d);
},
error: function (data) {
failure(data.responseJSON.error);
}
});
}
//Usage: create task
var taskProperties = {
'TaskName': 'Order Approval',
'AssignedToId': 12
};
createListItem('https://contoso.sharepoint.com/project/&#39;,'Tasks',taskProperties,function(task){
console.log('Task' + task.TaskName + ' has been created');
},
function(error){
console.log(JSON.stringify(error));
}
);

view raw
createListItem.js
hosted with ❤ by GitHub

Read

In order to  perform a Read operation via REST, you must perform the following actions:

  • Create an HTTP request using the GET verb.
  • Use the service URL of the list item to which you want to add an entity as the target for the GET.
  • Set the content type to application/json.

The code sample demonstrates of how to retrieve an item based on its ID:

function getListItemById(webUrl,listName, itemId, success, failure) {
var url = webUrl + "/_vti_bin/listdata.svc/" + listName + "(" + itemId + ")";
$.ajax({
url: url,
method: "GET",
headers: { "Accept": "application/json; odata=verbose" },
success: function (data) {
success(data.d);
},
error: function (data) {
failure(data.responseJSON.error);
}
});
}
//Usage
getListItemById('https://contoso.sharepoint.com/project/&#39;,'Tasks',2,function(taskItem){
console.log(taskItem.TaskName);
},
function(error){
console.log(JSON.stringify(error));
}
);

view raw
getListItemById.js
hosted with ❤ by GitHub

Update

To  update an existing entity, you must perform the following actions:

  • Create an HTTP request using the POST verb.
  • Add an X-HTTP-Method header with a value of MERGE.
  • Use the service URL of the list item you want to update as the target for the POST
  • Add an If-Match header with a value of the entity’s original ETag.

In contrast to reading list item  to update an item you will need to pass the eTag value, which could be obtained during item read.

About eTags

When updating or deleting items within SharePoint lists via REST you must specify the Entity Tag (eTag) value that was returned with the item during the initial query. This enables SharePoint to determine if the item has changed since it was requested. Alternatively you can tell SharePoint to perform the operation regardless by specifying * as the eTag value. For example:

  1. “If-Match”: item.__metadata.etag can be used to specify the actual eTag value (‘item’ is the object returned from SharePoint containing the list item in JSON format).
  2. “If-Match”: “*” can be used to match any eTag value resulting in the operation being performed regardless of the actual value.

They form part of the Ajax call for updating an item. eTags are part of the HTTP Protocol V1.1, more information on the topic can be found here.

function updateListItem(webUrl,listName,itemId,itemProperties,success, failure)
{
getListItemById(webUrl,listName,itemId,function(item){
$.ajax({
type: 'POST',
url: item.__metadata.uri,
contentType: 'application/json',
processData: false,
headers: {
"Accept": "application/json;odata=verbose",
"X-HTTP-Method": "MERGE",
"If-Match": item.__metadata.etag
},
data: Sys.Serialization.JavaScriptSerializer.serialize(itemProperties),
success: function (data) {
success(data);
},
error: function (data) {
failure(data);
}
});
},
function(error){
failure(error);
});
}
//Usage: set Task Name & AssignedTo
var taskProperties = {
'TaskName': 'Approval',
'AssignedToId': 12
};
updateListItem('https://contoso.sharepoint.com/project/&#39;,'Tasks',2,taskProperties,function(item){
console.log('Task has been updated');
},
function(error){
console.log(JSON.stringify(error));
}
);

view raw
updateListItem.js
hosted with ❤ by GitHub

Delete

To  delete an  entity, you must perform the following actions:

  • Create an HTTP request using the POST verb.
  • Add an X-HTTP-Method header with a value of DELETE.
  • Use the service URL of the list item you want to update as the target for the POST
  • Add an If-Match header with a value of the entity’s original ETag.

The Delete operation is similar to Update operation,  the code below demonstrates how to perform a Delete operation:

function deleteListItem(webUrl, listName, itemId, success, failure) {
getListItemById(webUrl,listName,itemId,function(item){
$.ajax({
url: item.__metadata.uri,
type: "POST",
headers: {
"Accept": "application/json;odata=verbose",
"X-Http-Method": "DELETE",
"If-Match": item.__metadata.etag
},
success: function (data) {
success();
},
error: function (data) {
failure(data.responseJSON.error);
}
});
},
function (error) {
failure(error);
});
}
//Usage: delete Task with Id=3
deleteListItem('https://contoso.sharepoint.com/project/&#39;,'Tasks',3,function(){
console.log('Task has been deleted');
},
function(error){
console.log(JSON.stringify(error));
}
);

view raw
deleteListItem.js
hosted with ❤ by GitHub

References

Working with Discussions List via SharePoint Client Object Model

Overview

In this post  will be demonstrated how to perform a common CRUD operations when working with Discussions List via  Client Object Model (JavaScript CSOM) in SharePoint 2010/2013.

Create a Discussion

So, let’s get started with creating a Discussion item. SP.Utilities.Utility.createNewDiscussion method is used for creating a discussion item:

function createDiscussion(listTitle,properties,OnItemAdded,OnItemError)
{
var context = new SP.ClientContext.get_current();
var web = context.get_web();
var list = web.get_lists().getByTitle(listTitle);
context.load(list);
var discussionItem = SP.Utilities.Utility.createNewDiscussion(context, list, properties.Subject);
for(var propName in properties) {
if(propName == 'Subject') continue;
discussionItem.set_item(propName, properties[propName])
}
discussionItem.update();
context.load(discussionItem);
context.executeQueryAsync(
function() {
OnItemAdded(discussionItem);
},
OnItemError
);
}

view raw
createDiscussion.js
hosted with ❤ by GitHub

Load all Discussions

Since Discussion Content Type derives from Folder Content Type  we could utilize SP.CamlQuery.createAllFoldersQuery method to construct a query to retrieve all the discussions:

function getDiscussions(listTitle,OnItemsLoaded,OnError)
{
var context = new SP.ClientContext.get_current();
var web = context.get_web();
var list = web.get_lists().getByTitle(listTitle);
context.load(list);
var qry = SP.CamlQuery.createAllFoldersQuery();
var discussionItems = list.getItems(qry);
context.load(discussionItems);
context.executeQueryAsync(
function() {
OnItemsLoaded(discussionItems);
},
OnError
);
}

view raw
getDiscussions.js
hosted with ❤ by GitHub

Create a Message

The SP.Utilities.Utility.createNewDiscussionReply method is used to create a Message item (add reply to a discussion item):

function createMessage(discussionItem,properties,OnItemAdded,OnItemError)
{
var context = new SP.ClientContext.get_current();
var messageItem = SP.Utilities.Utility.createNewDiscussionReply(context, discussionItem);
for(var propName in properties) {
messageItem.set_item(propName, properties[propName])
}
messageItem.update();
context.executeQueryAsync(
function() {
OnItemAdded(messageItem);
},
OnItemError
);
}
function createMessages(discussionItem,messagesProperties,OnItemsAdded,OnItemsError)
{
var context = new SP.ClientContext.get_current();
var messageItems = [];
$.each(messagesProperties, function (i, properties) {
messageItems.push(SP.Utilities.Utility.createNewDiscussionReply(context, discussionItem));
for(var propName in properties) {
messageItems[i].set_item(propName, properties[propName])
}
messageItems[i].update();
});
context.executeQueryAsync(
function() {
OnItemsAdded(messageItems);
},
OnItemsError
);
}

view raw
createMessage.js
hosted with ❤ by GitHub

Load all Messages

Since message items  are contained within discussion container (Folder) , to identify messages by discussion we will use SPBuiltInFieldId.ParentFolderId field in CAML query:

function getMessages(listTitle,disscussionId,OnItemsLoaded,OnError)
{
var context = new SP.ClientContext.get_current();
var web = context.get_web();
var list = web.get_lists().getByTitle(listTitle);
context.load(list);
var qry = createAllMessagesByDisscussionIDQuery(disscussionId);
var messageItems = list.getItems(qry);
context.load(messageItems);
context.executeQueryAsync(
function() {
OnItemsLoaded(messageItems);
},
OnError
);
}
function createAllMessagesByDisscussionIDQuery(disscussionId) {
var qry = new SP.CamlQuery;
var viewXml = "<View Scope='Recursive'> \
<Query> \
<Where> \
<Eq> \
<FieldRef Name='ParentFolderId' /> \
<Value Type='Integer'>" + disscussionId + "</Value> \
</Eq> \
</Where> \
</Query> \
</View>";
qry.set_viewXml(viewXml);
return qry;
};

view raw
getMessages.js
hosted with ❤ by GitHub

Import Discussion List

And finally in order to see how to work with Discussions List in action,  let’s discuss one more example.  Suppose we need to import Discussions List content from an external source, let’s say from Stack Exchange. To access Stack Exchange content (Questions and Answers in our case) we will utilize Stack Exchange API:

function DiscussionBoardImporter()
{
this.sourceQuery = '/2.1/questions?order=desc&sort=votes&site=sharepoint&filter=!-.AG)sL*gPlV';
this.targetDiscussionsListName = 'Discussions List';
}
DiscussionBoardImporter.prototype = (function() {
var importData = function(data,discussionsListName)
{
$.each(data, function (i, question) {
var discussionProperties = {'Body': question.body,'Subject':question.title,'VoteNumber': question.score};
createDiscussion(discussionsListName,discussionProperties,
function(discussionItem){
console.log('Discussion ' + discussionItem.get_item('Title') + ' has been created successfully');
var messagesProperties = [];
$.each(question.answers, function (j, answer) {
messagesProperties.push({'Title': answer.title,'Body': answer.body});
});
createMessages(discussionItem,messagesProperties,
function(){
console.log('Messages have been created successfully');
},
function(sender,args){
console.log('Error occured while creating messages:' + args.get_message());
}
);
},
function(sender,args){
console.log('Error occured while creating disscussion:' + args.get_message());
});
});
};
var loadData = function(query,targetListName, loaded)
{
$.ajax({
url: 'http://api.stackexchange.com&#39; + query,
dataType: 'json',
success: function(data) {
loaded(data.items,targetListName);
}
});
};
return {
constructor:DiscussionBoardImporter,
import: function()
{
loadData(this.sourceQuery,this.targetDiscussionsListName,importData);
}
};
})();
var importer = new DiscussionBoardImporter();
importer.import();

How to populate a SharePoint List via CSOM

Overview

Populating a SharePoint List with data is  required for different purposes quite often. In this article I am going to demonstrate how to populate Contacts list with a fake data using CSOM.

Provision column to a List via SharePoint CSOM

We will utilize Contacts List for demonstration purposes. But before we  proceed, let’s add a Geolocation field to a list.

Since the Geolocation column is not available by default in SharePoint lists, it’s common practice to provision this field to a list programmatically. The code below demonstrates  how to add the Geolocation field  by using the SharePoint client object model:

function ProvisionField(fieldSchema, listTitle,OnSuccess,OnError)
{
var context = SP.ClientContext.get_current();
var list = context.get_web().get_lists().getByTitle(listTitle);
var fields = list.get_fields();
var fieldToAdd = fields.addFieldAsXml(fieldSchema, true, SP.AddFieldOptions.AddToDefaultContentType);
fieldToAdd.update();
context.load(fieldToAdd);
context.executeQueryAsync(OnSuccess(fieldToAdd),OnError);
}
function AddGeolocationFieldToContactsList()
{
ProvisionField("<Field Type='Geolocation' DisplayName='Location'/>","Contacts",
function(field){
console.log('Geolocation field has been added');
},
function(sender,args){
console.log('Error occured while adding Geolocation field:' + args.get_message());
})
}

Generate list data

For populating Contacts list we will utilize Faker.js JavaScript library that is intended for generating massive amounts of fake data, below is provided the complete code:

function AddListItem(listTitle,itemProps,OnItemAdded,OnItemError)
{
var context = new SP.ClientContext.get_current();
var web = context.get_web();
var list = web.get_lists().getByTitle(listTitle);
var itemCreateInfo = new SP.ListItemCreationInformation();
var listItem = list.addItem(itemCreateInfo);
for(var propName in itemProps) {
listItem.set_item(propName, itemProps[propName])
}
listItem.update();
context.load(listItem);
context.executeQueryAsync(
function() {
OnItemAdded(listItem);
},
OnItemError
);
}
function GenerateContacts()
{
var contactsCount = 108;
for(var i = 0; i < contactsCount; i++){
var contactEntry = CreateContactEntry();
AddListItem('Contacts',contactEntry,
function(contactItem){
console.log('Contact ' + contactItem.get_item('Title') + ' has been created successfully');
},
function(sender,args){
console.log('Error occured while creating contact:' + args.get_message());
});
}
}
function CreateContactEntry()
{
var contactCard = Faker.Helpers.createCard();
return {'Title': contactCard.username,
'FullName': contactCard.name,
'Email': contactCard.email,
'Company': contactCard.company.name,
'WorkPhone': contactCard.phone,
'WorkAddress': contactCard.address.streetA,
'WorkCity': contactCard.address.city,
'WorkZip': contactCard.address.zipcode,
'WorkCountry': contactCard.address.ukCountry,
'WebPage': 'http://&#39; + contactCard.website,
'Location': 'Point(' + contactCard.address.geo.lng + ' ' + contactCard.address.geo.lat + ')'};
}
var scriptbase = _spPageContextInfo.siteAbsoluteUrl + '/_layouts/15/';
$.getScript(scriptbase + 'SP.js',
function () {
$.getScript(_spPageContextInfo.siteAbsoluteUrl + '/SiteAssets/Faker.js', GenerateContacts);
}
);

view raw
GenerateListData.js
hosted with ❤ by GitHub

Results

After running the script the Contacts list will be populated with a fake data as demonstrated below.

Contacts default view

Contacts

Contacts map view

ContactsMap

References

Embedding Video to a SharePoint Library

Overview

We have already discussed how to embed video from YouTube and another Video Providers into SharePoint, in particular it was demonstrated  how to:

In both cases some kind of customization should be applied for List/Library in order to render YouTube player in List Forms and Views. In first case custom fields are used for storing embed code properties and a computed field for rendering player. In the second case, custom fields are used for storing embed code itself and YouTube player is rendered via customized List View and Field.

This time we will discuss another approach that is intended for sharing video in out-of-the box List and Libraries, like Blogs and Discussion Boards.

How to embed Video in SharePoint Blog and Discussion Board

Before we will dive into technical details about implementation let me show some usage scenarios.

In order to embed a video into SharePoint:

  • On YouTube site click the Share button located under the video.
  • Click the Embed button.
  • Copy VideoId value from  src attribute value provided in the expanded box .
    EmbedYT
  • Add reply  for a Discussion and select Insert Tab – Share VideoYouTube Video Button to insert YouTube video on page
    ShareVideo_Ribbon
  • Specify YouTube Video parameters and click OK button to insert YouTube player on page
    ShareVideo_RteDialog
  • Saved Message item (for Discussion List)
    ShareVideo_Message
  • Blogs View
    ShareVideo_Blogs

Solution

The solution consist of:

  • SharePoint Ribbon and Page Component for Sharing Video
  • RTE with the capability to insert video player
  • Render video player on List Forms and Views

SharePoint Ribbon and Page Component for Sharing Video

1. The existing group Ribbon.EditingTools.CPInsert.Media is extended with the buttons for video sharing.

2. Video Sharing Page component is developed for interaction with the Server Ribbon:

Type.registerNamespace('MediaExtensions.Ribbon.RTE');
//Page Component for Sharing Video
MediaExtensions.Ribbon.RTE.VideoPageComponent = function () {
MediaExtensions.Ribbon.RTE.VideoPageComponent.initializeBase(this);
}
MediaExtensions.Ribbon.RTE.VideoPageComponent.prototype = {
mediaCommands: ['VideoGroup', 'ShareVideo', 'ShareVideoMenuOpen', 'ShareVideoMenuClose', 'EmbedVideo', 'InsertVideoWeb'],
registerWithPageManager: function () {
CUI.Page.PageManager.get_instance().addPageComponent(this);
}, getFocusedCommands: function () {
return this.mediaCommands;
}, getGlobalCommands: function () {
return this.mediaCommands;
}, canHandleCommand: function (commandId) {
return true;
}, handleCommand: function (commandId, properties, sequence) {
if (commandId === 'ShareVideo') {
this.invokeVideoCommand(commandId);
return true;
}
if (commandId === 'InsertVideoWeb') {
this.invokeVideoCommand(commandId);
return true;
}
return true;
}, isFocusable: function () {
return true
}, receiveFocus: function () {
return true;
}, yieldFocus: function () {
return true;
}, invokeVideoCommand: function (commandId) {
RTE.SnapshotManager.takeSnapshot();
var range = RTE.Cursor.get_range();
if (!range.get_isEditable()) {
return;
}
var dlg = new MediaExtensions.Ribbon.RTE.ShareVideoDialog();
dlg.show(commandId);
RTE.SnapshotManager.takeSnapshot();
}
}
MediaExtensions.Ribbon.RTE.VideoPageComponent.registerClass('MediaExtensions.Ribbon.RTE.VideoPageComponent', CUI.Page.PageComponent);
//Register Page Component for Sharing Video
NotifyScriptLoadedAndExecuteWaitingJobs("sharevideoribbon.js");
function ShareVideoInit() {
var currentPageManager = SP.Ribbon.PageManager.get_instance();
currentPageManager.addPageComponent(new MediaExtensions.Ribbon.RTE.VideoPageComponent());
}

view raw
sharevideoribbon.js
hosted with ❤ by GitHub

RTE with the capability to insert video player

By default  YouTube video player is rendered as IFrame element. Since in SharePoint 2010 it is not allowed to store iframe content into Publishing HTML field the alternative solution is proposed here:

1. User provides all  the properties for video in RTE dialog
ShareVideo_RteDialogOnly

2. Video player tag is inserted into SharePoint Rich Text Editor from RTE dialog:

<!– Video player tag is inserted into SharePoint Rich Text Editor from RTE Dialog –>
<!– When the List Form or View page is rendered, this tag is replaced with <iframe> (and video player) via YouTube IFrame API –>
<div id="ytplayer">
<span style="display: none;">Z6-iUNfJsJw;320;215</span> <!– player info in the following format VideoId;Width:Height –>
</div>

view raw
RTE_Video.html
hosted with ❤ by GitHub

3. When the List Form or View page is rendered, YouTube player is created and video is loaded.

Render video player on List Forms and Views

The IFrame player API is intended for embedding a YouTube video player on a website and control the player using JavaScript. The IFrame API posts content to an <iframe> tag on a page.

The code demonstrates how to create an embedded player that will load a video

var ytplayersinfolist = [];
//This function creates an <iframe> (and YouTube player)
//after the API code downloads
function onYouTubeIframeAPIReady() {
if (typeof window.playerInfoList === 'undefined')
return;
for (var i = 0; i < window.playerInfoList.length; i++) {
createYTPlayer(window.playerInfoList[i]);
}
}
// This function loads the IFrame Player API code asynchronously
function registerYTIFrameApi() {
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api&quot;;
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
}
function initYTPlayer(playerInfo) {
return new YT.Player(playerInfo.id, {
height: playerInfo.height,
width: playerInfo.width,
videoId: playerInfo.videoId
});
}

view raw
ytplayer.js
hosted with ❤ by GitHub

References

Integrating location and map functionality in SharePoint 2010

Overview

In SharePoint 2013 was  introduced a new field type named Geolocation that enables you to annotate SharePoint lists with location information. For example, you can now make lists “location-aware” and display latitude and longitude coordinates through Bing Maps. An entry is typically seen as a pushpin on a map view.

Due to the lack of these capabilities in SharePoint 2010 it was decided to fill the gap and bring location and map functionality into SharePoint 2010 and this is one the main reason why this project was initiated.

So, the aim of this project is to bring  location and map functionality into SharePoint 2010  (and more) the same way as they are currently available in SharePoint 2013.

Geolocation field

Geolocation field overview

SharePoint 2013 introduces a new field type named Geolocation that enables you to annotate SharePoint lists with location information.  The custom Geolocation field have been created as part of this project  that is intended for the same purposes in SharePoint 2010.

In columns of type Geolocation, you can enter location information as a pair of latitude and longitude coordinates in decimal degrees.

Add Geolocation column to a list

Geolocation column could be added to a list using standard capabilities of SharePoint 2010 UI as shown in Figure 1.

Figure 1. Add Geolocation column

CreategeoLocation

After the Geolocation column has been added, it could be used as shown below

Figure 2 New or Edit Form with Geolocation column
GeolocationNewForm

Figure 3. Display form with Geolocation column for Contacts list

GeolocationDispForm2

Figure 4. Representing Geolocation column in a list view

MapView_Popup2

Map view

Map View Overview

A map view is a SharePoint view that displays a map (with data obtained from the  Maps service ), using longitude and latitude entries from the Geolocation field type. When the Geolocation field type is available on the SharePoint list, a map view can be created  from the SharePoint UI. In the list, SharePoint 2010 displays the location on a map powered by Maps service, currently Bing Maps is supported only.

Create a map view

The following steps demonstrate how to create a map view from the SharePoint 2010 UI.

  1. Open the SharePoint 2010 list with Geolocation column.
  2. Choose Create view in Ribbon menu
  3. On the Choose a view type page, choose Map View, as shown in Figure 5.
    Figure 5. Choosing a view type
    MapViewFormat
  4. Save a view. A map view is created, as shown in Figure 6.
    Figure 6. Completed map view
    MapView

Resources

SharePoint 2010 Maps project is hosted on GitHub.

Customize the rendering of a List View in Sharepoint 2013: Displaying List Items in Accordion

Overview

We have already discussed how to create Accordion List View in SharePoint 2010 in this post.
SharePoint 2013 introduces client side rendering framework for List View that allows to define the rendering logic of SharePoint list views using HTML/JavaScript. So, let’s discuss how to create Accordion List View in SharePoint 2013 based on client-side rendering.

For demonstration purposes we will create FAQ List based on Custom List. Default view for our list  will look like this

FAQDefaultView

And our goal here to customize it in such a way list items will be displayed  in accordion as shown below

AccordionListView

Implementation

In order to simplify our solution we restrict ourselves to the creation of client side rendering template only and applying it to the existing List View. If you are interested how to build custom solution for Accordion List  please follow my post about creating Accordion List View for SharePoint 2010.

Create Custom List for FAQ

FAQ List is based on Custom List. For storing questions we will utilize Title field, for Answers we will create new field with Note type

Create  client-side rendered view for Accordion

Client-side rendered view of an FAQ list is presented below

(function () {
loadCss('http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.10/themes/redmond/jquery-ui.css&#39;);
function OnAccordionViewPostRender(renderCtx) {
jQuery(function() {
jQuery( "#accordionFAQ" ).accordion();
});
}
function loadCss(url){
var link = document.createElement('link');
link.href = url;
link.rel = 'stylesheet';
document.getElementsByTagName('head')[0].appendChild(link);
}
function OnAccordionViewPreRender(renderCtx) {
}
function RenderAccordionViewBodyTemplate(renderCtx) {
var listData = renderCtx.ListData;
if (renderCtx.Templates.Body == '') {
return RenderViewTemplate(renderCtx);
}
var accordionHtml ='';
accordionHtml = '<div id="accordionFAQ">';
for (var idx in listData.Row) {
var listItem = listData.Row[idx];
accordionHtml += '<h3>';
accordionHtml += listItem.Title;
accordionHtml += '</h3>';
accordionHtml += '<div>';
accordionHtml += listItem.Answer;
accordionHtml += '</div>';
}
accordionHtml += '</div>';
return accordionHtml;
}
function _registerAccordionViewTemplate() {
var accordionViewContext = {};
//accordionViewContext.BaseViewID = 'Accordion';
accordionViewContext.Templates = {};
accordionViewContext.Templates.View = RenderAccordionViewBodyTemplate;
accordionViewContext.OnPreRender = OnAccordionViewPreRender;
accordionViewContext.OnPostRender = OnAccordionViewPostRender;
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(accordionViewContext);
}
ExecuteOrDelayUntilScriptLoaded(_registerAccordionViewTemplate, 'clienttemplates.js');
})();

Customize a List View

The last step is to apply client-side rendering template to existing view. Let’s first add FAQ List instance on on page, after that  we only need to specify JSLink property value for List View

Please note that actually  three JavaScript files have been specified(the first two are jQuery libraries) for  JSLink property. All of them are stored in my case in hive, but of course they could be stored not only in file system.

Different ways of extending People Editor in the client side (SharePoint 2010)

Overview

Previously we have already discussed the possible ways for customization of  People Editor,in particular it was demonstrated  how to add  initialization and validation capabilities on the client side. This time we are going to concentrate on some aspects  of extensibility for  People Editor control that are available on the client side.

The entry point for extending of the People Editor control on the client side  is AfterCallbackClientScript property. It allows to specify the callback JavaScript function to be executed after People Entity value is resolved and  control is initialized.

//Callback function for People Editor AfterCallbackClientScript property
//ctx – People Editor Client Id
function AfterCallbackClientScript(ctx) {}

The different approach is to override Entity Editor callback function itself, for example the code below demonstrates how to execute method before control is initialized

//Override EntityEditorCallback method
var EntityEditorCallback = (function () {
var EntityEditorCallbackOrig = EntityEditorCallback;
return function() {
var result = arguments[0];
var ctx = arguments[1];
if(typeof PreCallbackClientScript != 'undefined')
PreCallbackClientScript(result, ctx);
EntityEditorCallbackOrig(result, ctx);
};
})();

The following example demonstrates the usage of the second approach.

Customize People Editor in Alerts subscription page to enable sending of  alerts assigned to SharePoint Group

Sending alerts assigned to SharePoint Group is not supported by default in SharePoint 2010. The main idea of this solution is to enable the selection of SharePoint Groups in people editor on subscription pages and the sending alerts to individual users from these groups

How it works

In Alert subscription page (New or Edit forms) type or select in People picker dialog box the SharePoint Group name

AlertsStep1

After clicking “Check names” button or OK button in People picker the SharePoint group will be  expanded  to individual users in the text box as shown below

AlertsStep2

Implementation

So, the solution consist of the following tasks:

  • activate SharePoint Groups  for selection in people editor control
  • expand a group entity to individual user entities in the text box

Enable SharePoint Group for People Editor via Delegate Control

As was noted earlier SharePoint Groups are not allowed for selection set  on people editor control in Alerts subscription pages

In order to activate  the selection of SharePoint Group in Alerts subscription pages the following control is intended (code behind):

public class SendAlertsManager : UserControl
{
#region Control Lifecycle
protected override void OnLoad(EventArgs e)
{
if (IsAlertNewPage || IsAlertEditPage)
{
EnableSendAlertsToSPGroup();
Visible = true;
}
else
{
Visible = false;
}
}
private void EnableSendAlertsToSPGroup()
{
var pe = FindControl<PeopleEditor>(Page.Controls);
if(pe!= null)
{
if(!pe.SelectionSet.Contains("SPGroup"))
{
pe.SelectionSet += ",SPGroup";
//pe.AfterCallbackClientScript = "expandGroupCallback";
}
}
}
protected override void OnPreRender(EventArgs e)
{
EnableSendAlertsToSPGroup();
base.OnPreRender(e);
}
#endregion
#region Control Utilities
/// <summary>
/// Find control recursivelly
/// (http://weblogs.asp.net/eporter/archive/2007/02/24/asp-net-findcontrol-recursive-with-generics.aspx)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="controls"></param>
/// <returns></returns>
public static T FindControl<T>(System.Web.UI.ControlCollection controls) where T : class
{
T found = default(T);
if (controls != null && controls.Count > 0)
{
for (int i = 0; i < controls.Count; i++)
{
if (found != null) break;
if (controls[i] is T)
{
found = controls[i] as T;
break;
}
found = FindControl<T>(controls[i].Controls);
}
}
return found;
}
#endregion
#region Properties
private bool IsAlertNewPage
{
get
{
return
(Context.Request.Url.PathAndQuery.IndexOf("_layouts/subnew.aspx",
StringComparison.InvariantCultureIgnoreCase) > 0);
}
}
private bool IsAlertEditPage
{
get
{
return
(Context.Request.Url.PathAndQuery.IndexOf("_layouts/subedit.aspx",
StringComparison.InvariantCultureIgnoreCase) > 0);
}
}
#endregion
}

view raw
SendAlertsManager.cs
hosted with ❤ by GitHub

Control with  reference to  JavaScript  library –  expand a group entity to individual user entities for picker editor control

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Control Language="C#" AutoEventWireup="true" Inherits="SharePoint.ControlExtender.WebControls.SendAlertsManager" %>
<script type="text/javascript" src="/_layouts/YASPP/entityeditorutils.js"></script>

Manifest file for registration of SendAlertsManager control via Delegate Control

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Control
Id="AdditionalPageHead"
Sequence="80"
ControlSrc="~/_controltemplates/SendAlertsManager.ascx">
</Control>
</Elements>

Expand SharePoint Group entity to individual Users entities (entityeditorutils.js)

Expand SharePoint Groups to individual User entities for People Editor

//Expand Group entity to to individual Users entities
function expandGroupEntity(groupEntity, fnCallback) {
var context = new SP.ClientContext.get_current();
var groups = context.get_web().get_siteGroups();
var groupId = getSPGroupIdForEntity(groupEntity);
var group = groups.getById(groupId);
this.users = group.get_users();
context.load(users);
context.add_requestSucceeded(onLoaded);
context.add_requestFailed(onFailure);
context.executeQueryAsync();
function onLoaded() {
var userEntities = '<Entities>';
var count = users.get_count();
for (i = 0; i < count; i++) {
var userItem = users.itemAt(i);
userEntities += UserEntryToXml(userItem);
}
userEntities += '</Entities>';
var results = GetEntities(userEntities);
fnCallback(groupEntity,results);
}
function onFailure(sender, args) {
fnCallback(groupEntity,null);
}
}
//Create Entity xml fron SPUser
function UserEntryToXml(userItem)
{
return '<Entity Key="' + userItem.get_loginName() + '" DisplayText="' + userItem.get_title() + '" IsResolved="True" Description="' + userItem.get_loginName() + '"><MultipleMatches /></Entity>';
}
function prepareUserEntities(result,ctx,fnCallback)
{
var entities=GetEntities(result);
if (entities==null)
return;
for(var x=0;x<entities.childNodes.length;x++)
{
var entity=entities.childNodes[x];
if(isSPGroupEntity(entity)) {
expandGroupEntity(entity,fnCallback);
}
else
fnCallback(null,null);
}
}
function getSPGroupIdForEntity(entity)
{
var groupId = 1;
var keys = EntityEditor_SelectNodes(entity,"Key");
for (var i=0;i<keys.length;i++) {
if(keys[i].firstChild.nodeValue == "SPGroupID") {
groupId = parseInt(keys[0].nextSibling.firstChild.nodeValue);
break;
}
}
return groupId;
}
function isSPGroupEntity(entity)
{
var isSPGroupEntity = false;
var keys = EntityEditor_SelectNodes(entity,"Key");
for (var i=0;i<keys.length;i++) {
if(keys[i].firstChild.nodeValue == "SPGroupID") {
isSPGroupEntity = true;
break;
}
}
return isSPGroupEntity;
}
var EntityEditorCallback = (function () {
var EntityEditorCallbackOrig = EntityEditorCallback;
return function() {
var result = arguments[0];
var ctx = arguments[1];
prepareUserEntities(result,ctx,function (groupEntity,userEntities) {
if (groupEntity != null) {
result = result.replace(EntitytoHtml(groupEntity),UserEntitiestoHtml(userEntities));
}
EntityEditorCallbackOrig(result, ctx);
});
};
})();
//Serialize user entities to Html
function UserEntitiestoHtml(entities)
{
var entityHtml = '';
for(i=0;i<entities.childNodes.length;i++)
entityHtml += EntitytoHtml(entities.childNodes[i]);
return entityHtml;
}
//Serialize entity to Html
function EntitytoHtml(entity)
{
var entityHtml = EntityEditor_XmlToString(entity);
entityHtml = entityHtml.replace(/<MultipleMatches\/>/g, '<MultipleMatches />');
return entityHtml;
}
//Entity Editor Helper methods
//select nodes from xml entity
function EntityEditor_SelectNodes(xmlNode, tagName)
{ULSGjk:;
//if(document.implementation && document.implementation.createDocument)
//{
var elems=xmlNode.getElementsByTagName(tagName);
if(elems.length > 0)
return elems;
return null;
//}
//else
//{
// return xmlNode.selectNodes(tagName);
//}
}
//Serialize Xml to String
function EntityEditor_XmlToString(xmlData) {
var xmlString;
//IE
if (window.ActiveXObject){
xmlString = xmlData.xml;
}
// code for Mozilla, Firefox, Opera, etc.
else{
xmlString = (new XMLSerializer()).serializeToString(xmlData);
}
return xmlString;
}

view raw
entityeditorutils.js
hosted with ❤ by GitHub