Showing posts with label SharePoint 2010. Show all posts
Showing posts with label SharePoint 2010. Show all posts

Tuesday, 23 December 2014

Using SharePoints SpellCheck Webservice with TinyMCE and AngularJS

In this post I want to demonstrate how to use SharePoint's SpellCheck webservice with the TinyMCE Richtext editor.

There are many reasons why you might choose to use the TinyMCE richtext editor over the Office365/SharePoint richtext editor. One of those reasons is if you're building a clientside app that needs a richtext editor, and you want the richtext controls to be inside the app, and not on the ribbon.

TinyMCE is a great richtext editor with good cross browser support. Among the functionality it has, it provides a way to integrate with a spell checking service.

In the example below I'm going to demonstrate how to integrate the SharePoint spellcheck.asmx webservice with the a TinyMCE richtext editor, in an AngularJS app, hosted in an Office365 or SharePoint site.

The files for this example can be downloaded from the MSDN TechNet Gallery, here: Technet Gallery. I suggest you download the files and look through the code to fully understand the example. To use the example, follow the instructions in the readme.txt file included in the zip file.

What I want to focus on here is how to call the webservice, and then how to interpret the results.

First, I'll start with a screenshot of what this looks like when it's running (the screen shots are from SharePoint 2013, but this works exactly the same with Office365):




The code to get this running looks like this  (remember, the full source can be downloaded here: TechNet Gallery):

First, the app's html file. It's pretty simple, containing a few script references and a textarea.

<!-- There's not much to the HTML file. A div that references my AngularJS controller, and then a textarea with the data-ui-tinymce attribute. -->
<div id="ng-app" data-ng-app="app" data-ng-cloak >
    <div data-ng-controller="spellCheckExampleCtrl as vm" data-ng-cloak>
        <textarea data-ui-tinymce id="eBriefTiming" data-ng-model="vm.richTextContent"></textarea>
    </div>
</div>
<!-- Load the scripts -->
<!-- XML2JSON is used to transfor the XML based response from the Spellcheck webservice to JSON -->
<script type="text/ecmascript" src="../tinymce/xml2json.js"></script>
<!-- AngularJS, Sanitize, resource and tinymce -->
<script type="text/ecmascript" src="../tinymce/angular.js"></script>
<script type="text/ecmascript" src="../tinymce/angular-sanitize.js"></script>
<script type="text/ecmascript" src="../tinymce/angular-resource.js"></script>
<script type="text/ecmascript" src="../tinymce/tinymce/tinymce.min.js"></script>
<!-- My scripts. All of this scripts are used to create the app. -->
<script type="text/ecmascript" src="../tinymce/config.tinymce.js"></script>
<script type="text/ecmascript" src="../tinymce/app.js"></script>
<script type="text/ecmascript" src="../tinymce/controllers.js"></script>
<script type="text/ecmascript" src="../tinymce/services.js"></script>

The app and controller code. Again, this is pretty simple; it's not really doing much in this simple app.

(function () {
    'use strict';
    var app = angular.module('app', [
    'ngSanitize',
    'ngResource',
    'ui.tinymce'
    ]);
    app.run();
})();

//App Controller
(function () {
    'use strict';
    //define the controller
    var controllerId = 'spellCheckExampleCtrl';
    angular.module('app').controller(controllerId, ['$scope', '$q', spellCheckExampleCtrl]);
    function spellCheckExampleCtrl($scope, $q) {
        var vm = this;
        vm.richTextContent = null;     
        init();
        function init() {           
        };
    }
})();

This next bit of code initialises the TinyMCE directive with AngularJS. The main point of interest in this code snippet is the spellchecker_callback function. Example the function, specifically how the results from the spelling webservice are interpreted.

(function () {
//pass in the remoteServices factory. This factory contains the method for querying the SharePoint Spellcheck webservice
angular.module('ui.tinymce', [])
.value('uiTinymceConfig', {})
.directive('uiTinymce', ['uiTinymceConfig', 'remoteServices', function (uiTinymceConfig, remoteServices) {
uiTinymceConfig = uiTinymceConfig || {};
var generatedIds = 0;
return {
require: 'ngModel',
priority: 10,
link: function (scope, elm, attrs, ngModel) {
var expression, options, tinyInstance;
var updateView = function () {
ngModel.$setViewValue(elm.val());
if (!scope.$root.$$phase) {
scope.$apply();
}
};
if (attrs.uiTinymce) {
expression = scope.$eval(attrs.uiTinymce);
} else {
expression = {};
}
if (expression.setup) {
var configSetup = expression.setup;
delete expression.setup;
}
// generate an ID if not present
if (!attrs.id) {
attrs.$set('id', 'uiTinymce' + generatedIds++);
}
options = {
// Update model when calling setContent (such as from the source editor popup)
setup: function (ed) {
ed.on('init', function (args) {
ngModel.$render();
ngModel.$setPristine();
});
// Update model on button click
ed.on('ExecCommand', function (e) {
ed.save();
updateView();
});
// Update model on keypress
ed.on('KeyUp', function (e) {
ed.save();
updateView();
});
// Update model on change, i.e. copy/pasted text, plugins altering content
ed.on('SetContent', function (e) {
if (!e.initial && ngModel.$viewValue !== e.content) {
ed.save();
updateView();
}
});
// Update model when an object has been resized (table, image)
ed.on('ObjectResized', function (e) {
ed.save();
updateView();
});
if (configSetup) {
configSetup(ed);
}
},
mode: 'exact',
elements: attrs.id,
inline_styles: true,
plugins: [
"advlist autolink lists link charmap hr pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime nonbreaking table contextmenu",
"paste textcolor spellchecker"
],
//override the default spellchecker (which, OOTB, doesn't work with SharePoint). Instead we'll use SharePoints spellcheck webservice.
//This method is documented here: http://www.tinymce.com/wiki.php/Configuration:spellchecker_callback
spellchecker_callback: function (method, text, success, failure) {
if (method == "spellcheck") {
//Call the checkSpelling method (this is a method we've defined in another file, documented in the next script block).
//This method will query the SharePoint spellcheck webservice. The query contains the full text from the
//TinyMCE richtext editor.
//When the response comes back, we need to create an array of spelling errors and suggestions
remoteServices.checkSpelling(text).then(function (data) {
var wordCollection = data;
var suggestions = [];
//***
//Check for spelling errors
//***
//Get the array of flagged words that are errors
var spellingErrors = null;
if (data.spellingErrors.SpellingErrors && data.spellingErrors.SpellingErrors.flaggedWords !== 'undefined') {
    spellingErrors = data.spellingErrors.SpellingErrors.flaggedWords.FlaggedWord;
}
//Check if an array of items was returned
if (spellingErrors instanceof Array == true) {
    for (var wi = 0; wi < spellingErrors.length; wi++) {
        var w = spellingErrors[wi];
        suggestions[w.word] = [];
    }
}
//Check if a single item was returned
else if (spellingErrors && spellingErrors.word) {
    suggestions[spellingErrors.word] = [];
}
//***
//Check for spelling suggestions
//***
//Get the array of flagged words with suggestions
var spellingSuggestions = null;
if (data.spellingSuggestions.SpellingSuggestions) {
    spellingSuggestions = data.spellingSuggestions.SpellingSuggestions;
};
//Check if an array of items was returned
if (spellingSuggestions instanceof Array == true) {
    for (var wi = 0; wi < spellingSuggestions.length; wi++) {
        var w = spellingSuggestions[wi];
        //Check if there is a single spelling suggestion, or an array of suggestions.
        //Then add it to suggestions array for the current word
        suggestions[w.word] = (w.sug.string instanceof Array == true) ? w.sug.string : [w.sug.string];
    }
}
//Check if a single item was returned
else if (spellingSuggestions && spellingSuggestions.word) {
    //Check if there is a single spelling suggestion, or an array of suggestions.
    //Then add it to suggestions array for the current word
    suggestions[spellingSuggestions.word] = (spellingSuggestions.sug.string instanceof Array == true) ? spellingSuggestions.sug.string : [spellingSuggestions.sug.string];
}
//Return the list of suggestions to the success handler.
success(suggestions);
})["catch"](function (error) {
//in my testing, failure doesn't seem to work. So I'm sending back Success with a null value.
success(null);
});
}
},
toolbar: "styleselect | bold italic | bullist numlist outdent indent | link | spellchecker",
fontsize_formats: "9pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt",
menubar: true,
statusbar: false,
height: 300,
width: 620
};
angular.extend(options, uiTinymceConfig, expression);
setTimeout(function () {
tinymce.init(options);
});
ngModel.$render = function () {
if (!tinyInstance) {
tinyInstance = tinymce.get(attrs.id);
}
if (tinyInstance) {
tinyInstance.setContent(ngModel.$viewValue || '');
ngModel.$setPristine();
}
};
scope.$on('$destroy', function () {
if (!tinyInstance) { tinyInstance = tinymce.get(attrs.id); }
if (tinyInstance) {
tinyInstance.remove();
tinyInstance = null;
}
});
}
};
}]);
})();

Finally, the code for the remoteServices factory. This code is responsible for making the call to SharePoint's Spellchecker webservice.

(function () {
'use strict';
var serviceId = 'remoteServices';
angular.module('app').factory(serviceId, ['$resource','$q', remoteServices]);
function remoteServices($resource, $q) {
    var service = this;       
    init();
    //service signature
    return {           
        checkSpelling: checkSpelling
    };
    function init() {           
    }
    //This function returns a resource used to query
    //the spellcheck webservice. It contains the HTTP method,
    //headers and will transform the response from XML to JSON
    function getSpellCheckerResource() {
        return $resource('/_vti_bin/spellcheck.asmx',
        {}, {
            post: {
                method: 'POST',
                params: {
                    'op': 'SpellCheck'
                },
                headers: {
                    'Content-Type': 'text/xml; charset=UTF-8'
                },
                transformResponse: function (data) {
                    // convert the response data to JSON
                    // before returning it
                    var x2js = new X2JS();
                    var json = x2js.xml_str2json(data);
                    return json;
                }
            }
        });
    }
             
    //This is the public function the TinyMCE editor will
    //call when the check spelling button is clicked.
    //The functions takes a block of text (or words) as input
    //and returns the spellcheck results
    function checkSpelling(words) {
    //Convert an array of words into a single string
    var wordstring = "";
    for(var i = 0; i < words.length; i++)
    {
        wordstring += (words[i] + ' ');
    }          
    //build the SOAP request
    var soapData = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><SpellCheck xmlns="http://schemas.microsoft.com/sharepoint/publishing/spelling/"><chunksToSpell><string>' + wordstring + '</string></chunksToSpell><declaredLanguage>3081</declaredLanguage><useLad>false</useLad></SpellCheck></soap:Body></soap:Envelope>'
    //Get the resource (defined in a function above) used to query the webservice
    var resource = getSpellCheckerResource();
    var deferred = $q.defer();
    //Post the data (the string of words) to the webservice 
    //and wait for a response!
    resource.post(soapData, function (data) {
        //successful callback          
        deferred.resolve(data.Envelope.Body.SpellCheckResponse.SpellCheckResult);
    }, function (error) {
        //error callback
        var message = 'Failed to queried the SharePoint SpellCheck webservice. Error: ' + error.message;
        deferred.reject(message);
    });
        return deferred.promise;
    }
}
})();

The screenshots below (taken from Fiddler and Chrome) show the request being sent and the data that is received back.

Request.


Response (showing the errors).


Response (showing the suggestions).



You can inspect the array of spelling suggestions and errors received back from the webservice by putting a break in the code.




Hpapy Spallnig!

References:

Wednesday, 25 June 2014

Script Creating SharePoint User MySites using PowerShell

While working on a large migration project (SharePoint 2010 to SharePoint 2013), I had a requirement to script the creation of user MySites for hundreds of users. I knocked this little PowerShell function together to do just that!

Function Create-MySite            
{            
 [CmdletBinding()]            
 Param            
 (            
  [Parameter(Mandatory = $True,Position=2,valueFromPipeline=$true)][String]$Username,            
     [Parameter(Mandatory = $True,Position=1)][String]$MySiteRootURL              
 )            
 [void][reflection.assembly]::Loadwithpartialname("Microsoft.Office.Server");               
 $site=new-object Microsoft.SharePoint.SPSite($MySiteRootURL);            
 try            
 {            
  $serviceContext = Get-SPServiceContext $site;            
  $upm = new-object Microsoft.Office.Server.UserProfiles.UserProfileManager($serviceContext);             
  if($upm.UserExists($Username) -eq $false)            
  {            
   Write-Host "User $Username was not found in the profile store." -f yellow;            
   return;            
  }            
  $userProfile = $upm.GetUserProfile($Username);            
  if($userProfile.PersonalSite -eq $Null)            
  {            
   Write-Host "Creating MySite for user $Username" -f darkyellow;            
   $userProfile.CreatePersonalSite();                  
   Write-host "Successfully created MySite for user $Username" -f green;            
  }            
  else            
  {            
   Write-Host "User $Username already has a MySite." -f darkgreen;            
  }            
 }            
 catch            
 {            
  Write-Host "Encountered an error creating a MySite for user $Username. Error:"$_.Exception -f Red;            
 }            
 finally            
 {            
  $site.Dispose();            
 }            
}

You can call the function like this (for a single user):

#Example - Create a MySite for user  "tonyj"            
Create-MySite -MySiteRootURL "http://mysite.bigintranet.com.au" -Username "tonyj"

Or call it like this, for a batch of users:

#To Create for an array of users            
$users = @('tonyj','bobh','markf','billd')            
$users | Foreach {Create-MySite -MySiteRootURL "http://mysite.bigintranet.com.au" -username $_}

Monday, 16 June 2014

Recursively Disabling a SharePoint Feature throughout a Farm

I'm working on a large SharePoint migration project at the moment. It's required quite a bit of PowerShell to automate tasks that ensure the project is a success.

One of those tasks was to find everywhere a feature was activated through the farm, and optionally disable the feature. Whenever we disable a feature throughout the farm, we need to keep a report of all the places the feature was previously enabled (there's about ten thousand webs in this farm).

So I wrote a script, that does exactly that! The script takes a feature-id, an SPWeb and two switches as parameters, and returns an object collection that records details about the feature instances disabled (feature id and display name, the web url, the feature status and the time it was last activated).

The two parameters, -Recurse, and -Report, control the scripts function. The -Recurse parameter tells the script to check all of the input webs sub-webs. The -ReportOnly parameter tells the script to... well, create a report of everywhere the feature is currently active, without actually disabling it!

Here's the function:

function Outst-SPFeature            
{            
 [CmdletBinding()]                        
    Param(                          
            [parameter(Mandatory=$true,Position = 0,valueFromPipeline=$true)][Microsoft.SharePoint.SPWeb]$Web,            
            [parameter(Mandatory=$true)][string]$FeatureId,            
            [parameter(Mandatory=$false)][switch]$Recurse,            
   [parameter(Mandatory=$false)][switch]$ReportOnly            
        )            
 #Define an object that can store the features properties             
 $FeatureInfo = New-Object psobject            
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "Id" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "DisplayName" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "WebUrl" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "Status" -value ""
 $FeatureInfo | Add-Member -MemberType NoteProperty -Name "TimeActivated" -value ""             
 #create an empty array variable            
 $matches = @();            
             
 Write-Host "Checking web"$Web.Url -f Green -b Yellow;#pipe the list of features ($web.Features) to the Where-Object cmdlet (aliased as '?'), and look for a feature with the same FeatureId that was passed to the script as a parameter            
 $f = $Web.Features | ?{$_.DefinitionId -like $FeatureId}            
    #If the feature was found, $f won't be null            
 if($f -ne $null)            
 {            
        #If the feature was found, check the SPFeature.Definition.Status property, to see if the feature is activated (online)            
  if($f.Definition.Status -eq "Online")            
  {            
   Write-Host "Disabling feature,"$f.Definition.DisplayName -f Green            
            #If the feature is online, then record the features details in a new custom object (this will get returned at the end of the function)            
   $fm = $FeatureInfo | Select-Object *;            
   $fm.Id = $f.Definition.Id;            
   $fm.DisplayName = $f.Definition.DisplayName;            
   $fm.WebUrl = $Web.Url;            
   $fm.Status = $f.Definition.Status;            
   $fm.TimeActivated = $f.TimeActivated            
   #Add the custom object to the $matches array (a list that will contain a custom object for each instance of a feature found)            
   $matches += $fm;            
    #Check the -ReportOnly switch. If the switch wasn't passed to the function, it will be false.             
   if($ReportOnly -eq $false)            
   {            
                #Disable the feature            
    Disable-SPFeature -Identity $f.Definition.Id -Url $Web.Url -Confirm:$false
   }               
  }            
 }            
  #Check the -Recurse switch. If the switch wasn't passed to the function, it will be false. Also check if the web has any sub-webs. If both of these checks are true, then check the sub-webs            
 if($Recurse -eq $true -and $Web.Webs.Count -gt 0)            
 {            
  Write-Host "Checking sub webs" -f Blue            
  foreach($sw in $Web.Webs)            
  {   #Here we want to call the same function again, against the sub-webs. The $matches variable is incremented with the results returned from the new call to the function.             
   if($ReportOnly -eq $false)            
   {            
    $matches += Outst-SPFeature -Web $sw -FeatureId $FeatureId -Recurse            
   }            
   else            
   {            
    $matches += Outst-SPFeature -Web $sw -FeatureId $FeatureId -Recurse -ReportOnly            
   }                 
  }              
 }            
 #The $matches array (containing the list that contains custom objects for each instance of a feature found) is returned.            
 return $matches;            
}            

And this is how it can be called:

#Run this command to disable the feature on all webs in all webapplications.            
$disabledfeature = Get-SPWebApplication | Get-SPSite -Limit All | Get-SPweb -Limit All |Foreach {Outst-SPFeature $_ -FeatureId "a5557f3e-102c-401e-9eae-1e7fcf4340d0" -Recurse}

And this is how you export the results to an xml file that can reloaded at a later time:

#Run this command to disable the feature on all webs in all webapplications, and save a report of all the feature instances that were disabled to C:\Temp\DisabledFeatureReport.xml
Get-SPWebApplication | Get-SPSite -Limit All | Get-SPweb -Limit All |Foreach {Outst-SPFeature $_ -FeatureId "a5557f3e-102c-401e-9eae-1e7fcf4340d0" -Recurse} | Export-Clixml -Path C:\Temp\DisabledFeatureReport.xml