Saturday, December 19, 2015

scanning for keywords as you type

If you need to detect the presence of keywords as you type, you can add a small amount of javascript to your client. This may be useful to detect the typing of tags and to populate your data model with structured data.
For example, if you want to detect football teams while you type, you could do something like this. You'll want to ensure your data model has been updated to have two-value Yes/No fields indicated by the uitests_ prefix.
// Provide a handler that can find hashtag keywords in a text box and update a data model. The keywords 
// are mapped to field names using a simple data structure. Everything here is hard-coded
// to help demonstrate the basic idea.

// We use methods that are easy to program for the POC but are not supported on all devices
// however, this is a way to do this for all devices, but let's be lazy this time.

// Mapping between hashtags (starting with #) and field names.
// Normally this is stored in a custom entity or uses some other scheme...
var mappings = {
"Ravens" : "uitests_Ravens",
"Patriots" : "uitests_Patriots",
"SF" : "uitests_SF"
};

// This could also be a call out to a web service but additional
// logic to make the call out efficient would be needed.
function attachChangeHandlers(controlName) {
    controlName = "description"
    var controlNameDOM = "description_i"

    // Process content scanning for hashtags.
    function processContent(content) { 
        for(hashtag in mappings) {
                if(!mappings.hasOwnProperty(hashtag)) { continue; }
                var attributeToSet = mappings[hashtag];            
                if(attributeToSet && content) {
                    if(~content.toLowerCase().indexOf("#" + hashtag.toLowerCase())) {                
                        Xrm.Page.getAttribute(attributeToSet.toLowerCase()).setValue(true);
                    }
                }
            }
    }


    // Normally we would define a namespace for our customizations...
    // This checks the map on every keystroke which is inefficient
    // but this can easily be optimized.
    // For 2016, can have the context passed in ;-)
    var keywordHandler = function() {
        try { 
            // CRM 2016 only! If you use 2015, you don't need domControl passed in.
            //var userInput = Xrm.Page.ui.controls.get(controlName).getValue();
            var userInput = this.value
            processContent(userInput)
        } catch(ex) {
            console.log(ex);
        }
        }


    // CRM 2016 has an API for key presses and getting the value of a control
    // as you type...use that after you upgrade because its more portable.
    //Xrm.Page.getControl(name).addOnKeyPress(keywordHandler)

    // The hard way...get the doc inside the MS CRM Dynamics IFrame.
    // parent.parent.document gets us to the topmost level.
    var doc = parent.parent.document.getElementById("contentIFrame0").contentWindow.document
    var inputControl = doc.getElementById(controlNameDOM);
    inputControl.addEventListener("keyup", keywordHandler);

    // Attach to the rich text editor as well
    window.setTimeout(function() { 
    var CKE = parent.parent.document.getElementById("contentIFrame0").contentWindow.CKEDITOR;
    if(CKE) {
        var richEditor = CKE.instances["new_richdescription_i"];
        if(richEditor) { 
            console.log("Found rich editor");
            richEditor.on('key', function() {
                processContent(richEditor.getData());
            });
        } else { 
            console.log("Did not find rich text editor!");
        }
    } else { 
        console.log("No CKEDITOR object found");
    }}, 2000);
}
Now in your form load event, call the setup function. As you type, the keywords are detected and the data model changed. Of course, you would usually hookup the keyword to field name mapping as an entity and pull it into the form when it loads, but that's an exercise for the reader.
The handler is attached to both a standard description box as well as a rich text description box (see my otehr blog). The delay is there with an overly generous delay to allow the CKEDITOR to load before the handler is attached. There's probably another way to do it, but most people will attach to the standard description box anyway so ignore that part of teh code.
MS CRM 2016 has API to allow attached key press handlers to controls, so some of the shanigans in this code are not needed in 2016 (yeah!).
With this setup, you can now detect twitter like hashtag words in text and change your data model e.g. "John mentioned that he likes #SF and the #Patriots" sets these attributes to true in the data model in real-time. This is convenient if someone is taking notes but they do not want to move their hands or finds tagging the record too burdonsome. There are "tag" oriented solutions you can add that add a nice tag model to your data model so you may want to look into those as well.

Saturday, December 5, 2015

Adding Rich Text Editor

Many people want a rich text editor in their CRM views to edit descriptions or notes. Solving this problem in general is hard across all devices such as desktop web and mobile, but here's a start.
First, look at this blog. It provides the general idea.
I had problems with this working for me, so I had to change the code to:
// Function that when called, loads the CKEditor from a CDN and converts some specially named fields...modify to meet
// the needs of your script block.

function convertToRichText() {

    // import the ckeditor script, but do it without web resources..  so we create a script tag in the DOM
    var headblock = parent.parent.document.getElementById("contentIFrame0").contentWindow.document.getElementsByTagName('head')[0];
    var newscriptblock = parent.parent.document.getElementById("contentIFrame0").contentWindow.document.createElement('script');
    newscriptblock.type = 'text/javascript';
    // we have to wait until the script is loaded before we can use it, so registering a callback event
    newscriptblock.onload = function() {

        // some configuration for the CKEDITOR rich text control
        var CKE = parent.parent.document.getElementById("contentIFrame0").contentWindow.CKEDITOR;
        CKE.config.allowedContent = true
        CKE.config.toolbarCanCollapse = true;
        CKE.config.toolbarStartupExpanded = false;
        CKE.config.width = '95%';

        var fieldsToReplace = ["new_richdescription"];
        for (var i = 0; i < fieldsToReplace.length; i ++) {
            var fieldname = fieldsToReplace[i];
            // We find the 'edit' control for the engagement overview and replace it with a rich text control
           var richtexteditor = CKE.replace(fieldname + '_i');

        richtexteditor.on('change', function() { 
          Xrm.Page.data.entity.attributes.get(fieldname).setValue(richtexteditor.getData());
        });
        richtexteditor.on('loaded', function ( field ) {
                // when the rich text control is done loading, we need to change the display so it shows the rich text - this covers the page load scenario
                $('#contentIFrame0', window.top.document).contents().find('div#' + field + ' div.ms-crm-Inline-Value').css('display', 'none');
                $('#contentIFrame0', window.top.document).contents().find('div#' + field + ' div.ms-crm-Inline-Edit').css('display', 'inline-block');
                $('#contentIFrame0', window.top.document).contents().find('div#' + field + ' div.ms-crm-Inline-Edit').css('width', '95%');

            }(fieldname));
        }
    };
    newscriptblock.src = '//cdn.ckeditor.com/4.5.5/standard/ckeditor.js';
    headblock.appendChild(newscriptblock);
}
And then it worked. However, I found it klunky so you need to ensure that you build a robust solution that works on all devices. You'll want to make this more robust in several ways but that's the gist of it.
Note that the Xrm toolkit recently released a rich text editor based on CKEditor. That's described here.