Monday, February 3, 2014

Mixing It Up: SharePoint JSOM/REST with HTML5 Canvas

I've been fooling with SharePoint client-side APIs since the end of last year.  What’s there not to love, I say. It sure makes up for the awful feeling I had at the beginning of my SharePoint 2013 developer refresh training, where I learned that .NET API calls, the same .NET API calls I had grown to love, would be deprecated in future SharePoint releases. Gone were those “salad days” of GACing DLLs in farm solutions. I guess that’s just a part of life, huh?  Things change, and the better adept you are at handling change, the better off you will be. The loss of .NET-based SharePoint solutions is actually a a good thing. Almost everything we could do with managed code can be done with client-side scripting.
 
Because I like teaching myself things, I decided to look into the HTML5 Canvas and see how I could build a graphic interface to a SharePoint “Task” list. This was the perfect learning opportunity.  It turned out.. well, you decide.
 
canvas1
 
I call this my “Power Wedges” (TM). This is a graphic representation of a SharePoint “Task” list. The wedges in the circle are created on the fly through the scripting process.  Rows of Task list data come back from a REST call and are iterated over to create the wedges. There are six tasks in the list in this view. If there were seven tasks, there would be seven evenly spaced wedges. Clicking on a wedge changes the MetaWedge in the middle and the rectangular box on the right. The MetaWedge is a series of arcs crafted to allow an AJAX JSOM call to update the Task “Percent Complete” field per list item.  On mouse over the various arcs changes a callout message in the mock interface. It’s rudimentary but can help one visualize other rich graphic interfaces for boring SharePoint activities.
 
The scripting logic depends on the KineticJS library of HTML5 Canvas tools.  There’s also a dependency on the ubiquitous jQuery.
 
This is the markup required for targeting of Canvas elements.  (There’s the link to the KineticJS library as well.)
 
 
<div id="container" style="background-color:gray"></div>

<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v5.0.1.min.js"></script>

<script defer="defer" src="wedges.js"></script>

 

The script (I saved this in a file called wedges.js) is complex but easy. 

  • The script runs, and a new Stage is created (line 1)  This stage will be used to layers.
  • I create the basic 300 radius circle which will be the placeholder for wedges. (line 17)
  • I create the MetaWedge placeholder and Details Rectangle. (line 23 & 24)
  • On line 27 I call a function to getTasks()
  • The getTask function (line 110) does a REST call to “Tasks” and passes a dataset to a success handler.
  • The success handler “_drawCoreWedges” (line 138) does calculations, iterates over the dataset and creates the clickable wedges in the left circle.
  • The “createWedge” function takes arguments such as angle, fill color, rotation, text message, and item.ID. (line 34) These arguments are used for “on” events.
  • The MetaWedge creates click handlers and passes parameters to “MarkComplete” function (line 126)
  • “MarkComplete” is a JSOM call that updates Task items.

 

var stage = new Kinetic.Stage({  container: 'container', width: 2000, height: 800, });



var midX = 360, midY = 360;



var coreLayer = new Kinetic.Layer();



//add pointer calcs

stage.on('mousemove', function () {

    var mousePos = stage.getPointerPosition();

    var x = mousePos.x, y = mousePos.y;

    writeMessage('x: ' + x + ', y: ' + y, coreLayer);

});



var text = new Kinetic.Text({ x: 10, y: 10, fontFamily: 'Calibri', fontSize: 24, text: '', fill: 'black' });

coreLayer.add(text);



createCircle(300, 'white', 2, coreLayer);



// add the layer to the stage

stage.add(coreLayer);



//init the meta-wedge

createMetaWedge('transparent');

createDetailsRectangle('transparent', 'Select a wedge and the details with appear here.');



//get items from Tasks list

getTasks();



function createCircle(radius, fill, stroke, layer) {

    var circle = new Kinetic.Circle({ x: midX,  y: midY, radius: radius, fill: fill, stroke: 'black', strokeWidth: stroke });

    layer.add(circle);

}



function createWedge(angle, fill, rotation, text, itemId) {

    var layer = new Kinetic.Layer();



    var wedge = new Kinetic.Wedge({ x: midX, y: midY, radius: 300, angle: angle, fill: fill, stroke: 'black', strokeWidth: 2, rotation: rotation, opacity: 0.5 });

    wedge.on('mouseover', function () {

        this.opacity(1);

        layer.draw();

    });

    wedge.on('mouseout', function () {

        this.opacity(0.5);

        layer.draw();

    });

    wedge.on('click', function () {

        createMetaWedge(fill, itemId);

        createDetailsRectangle(fill, text);

    });

    layer.add(wedge);



    stage.add(layer);

}



function createMetaWedge(fill, itemId) {

    var layer = new Kinetic.Layer();



    var wedge = new Kinetic.Wedge({ x: 800, y: 500, radius: 300, angle: 30, fill: fill, stroke: 'black', strokeWidth: 2, rotation: 255 });

    layer.add(wedge);



    var complete100 = new Kinetic.Wedge({ x: 800, y: 500, radius: 200, angle: 30, fill: 'transparent', stroke: 'black', strokeWidth: 2, rotation: 255 });

    complete100.on('mouseover', function () { statusText.setText("Mark 100% Complete..."); layer.draw(); });

    complete100.on('mouseout', function () { statusText.setText(""); layer.draw(); });

    complete100.on('click', function () { MarkComplete(itemId, 100); statusText.setText("Set to 100% Complete..."); layer.draw(); });

    layer.add(complete100);



    var complete75 = new Kinetic.Wedge({ x: 800, y: 500, radius: 150, angle: 30, fill: 'transparent', stroke: 'black', strokeWidth: 2, rotation: 255 });

    complete75.on('mouseover', function () { statusText.setText("Mark 75% Complete..."); layer.draw(); });

    complete75.on('mouseout', function () { statusText.setText(""); layer.draw(); });

    complete75.on('click', function () { MarkComplete(itemId, 75); statusText.setText("Set to 75% Complete..."); layer.draw(); });

    layer.add(complete75);



    var complete50 = new Kinetic.Wedge({ x: 800, y: 500, radius: 100, angle: 30, fill: 'transparent', stroke: 'black', strokeWidth: 2, rotation: 255 });

    complete50.on('mouseover', function () { statusText.setText("Mark 50% Complete..."); layer.draw(); });

    complete50.on('mouseout', function () { statusText.setText(""); layer.draw(); });

    complete50.on('click', function () { MarkComplete(itemId, 50); statusText.setText("Set to 50% Complete..."); layer.draw(); });

    layer.add(complete50);



    var complete25 = new Kinetic.Wedge({ x: 800, y: 500, radius: 50, angle: 30, fill: 'transparent', stroke: 'black', strokeWidth: 2, rotation: 255  });

    complete25.on('mouseover', function () { statusText.setText("Mark 25% Complete..."); layer.draw(); });

    complete25.on('mouseout', function () { statusText.setText(""); layer.draw(); });

    complete25.on('click', function () { MarkComplete(itemId, 25); statusText.setText("Set to 25% Complete..."); layer.draw(); });

    layer.add(complete25);

    



    var statusText = new Kinetic.Text({ x: 720, y: 520, width: 200, text: "", padding: 10, fontFamily: 'Calibri', fontSize: 20, fill: 'black' });

    layer.add(statusText);



    stage.add(layer);

}



function createDetailsRectangle(fill, text) {

    var layer = new Kinetic.Layer();



    var rect = new Kinetic.Rect({ x: 1000, y: 200, width: 600, height: 300, fill: fill, stroke: 'black', strokeWidth: 2 });

    layer.add(rect);



    var text = new Kinetic.Text({ x: 1010, y: 210, fontFamily: 'Calibri', fontSize: 24, text: text, fill: 'black', width: 580, padding: 20 });

    layer.add(text);



    stage.add(layer);

}





function writeMessage(message, layer) {

    text.setText(message);

    layer.draw();

}



function getTasks() {

    var url = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getbytitle('Tasks')/items";

    $.ajax({

        url: url,

        method: "GET",

        headers: { "Accept": "application/json; odata=verbose" },

        success: function (data) {

            _drawCoreWedges(data.d.results);

        },

        error: function (data) {

            console.log("Error obtaining Configuration Item");

            console.log(data);

        }

    });

}



function MarkComplete(itemId, percent) {

    var ctx = SP.ClientContext.get_current();

    var tasks = ctx.get_web().get_lists().getByTitle('Tasks');

    var item = tasks.getItemById(itemId);

    item.set_item('PercentComplete', percent / 100);

    item.update();

    ctx.executeQueryAsync(Function.createDelegate(this, update_Success),

                          Function.createDelegate(this, update_Fail));

}

function update_Success(sender, args) { /*it's done...*/ }

function update_Fail(sender, args) { alert("Failed to update item..."); }



function _drawCoreWedges(items) {

    if (items.length > 0) {

        var numberOfWedges = items.length;

        var wedgeSize = 360 / numberOfWedges;

        var posWedge = 0;

        for (var i = 0; i < items.length; i++) {

            var randColor = '#' + Math.floor(Math.random() * 16777215).toString(16); //random colorer

            var item = items[i];

            createWedge(wedgeSize, randColor, posWedge, item.Title + " - " + stripHTML(item.Body), item.Id);

            posWedge += wedgeSize;

        }

    }

}



function stripHTML(orig) {

    if (orig) {

        return orig.replace(/(<([^>]+)>)/ig, "");

    }

    return "";

}