Saturday, January 10, 2015

Custom SharePoint 2013 Star Ratings with JSOM and SVG

Star Ratings for SharePoint 2013 Publishing Pages are boring. I decided I wanted to make them cool and learn some JSOM to boot. What’s JSOM you ask?  It’s the JavaScript version of SharePoint’s Client-Side Object Model (CSOM). Basically, it allows you to interact with SharePoint data with a client-side script.

I wanted to make my custom graphics out of SVG, aka Scaled Vector Graphics, because it would add to the coolness factor. SVG is now supported by all modern browsers and adheres to the “last 2 major browser versions” compatibility rule. (For example, we’re currently on IE11, and SVG was available for IE9.) I won’t rant too much about IE8 users. If you’re trying to use SharePoint 2013 using IE8, consider quitting your job, growing out your hair, moving to Ft. Lauderdale, and opening a hot dog stand.

SVG graphics are defined with XML. This XML is interpreted by your browser and, like HTML, is converted into a graphical representation. I decided early that my Star Ratings graphics would be “O” from the Rightpoint company logo (and why not):

RP_Circle_300

I had the graphic in JPG format. I found an online converter that would take my JPG and make it an SVG file.  Here’s what it spit out:

 

 

 

 

<svg width="300" height="300">

<path style="fill:#e9e6e2; stroke:none;" d="M0 0L0 300L300 300L300 0L0 0z"/>

<path style="fill:#ec602f; stroke:none;" d="M86 145L41 172C44.8996 190.654 55.4326 209.351 68.9498 222.714C81.7013 235.319 96.0781 244.326 113 250.213C124.493 254.211 135.956 255 148 255C241.656 255 291.97 140.203 228.925 71.0008C219.869 61.0607 208.468 51.9277 196 46.615C176.027 38.1039 155.83 34.0572 134 36.1698C100.562 39.4057 67.4251 61.291 51.7454 91C48.5677 97.0209 39.3163 110.509 41.7076 117.671C42.9506 121.395 49.8439 124.188 53 126.078L86 145z"/>

<path style="fill:#e9e6e2; stroke:none;" d="M144 86.4244C132.754 87.8829 123.584 89.2856 114 96.0432C78.376 121.162 80.7516 176.363 119 197.956C131.061 204.765 144.632 205.509 158 203.844C165.291 202.936 171.672 200.225 178 196.572C229.208 167.013 205.541 78.4429 144 86.4244z"/>

</svg>

 

Viewing this XML markup in a browser showed the logo. Zooming in on it showed absolutely no mosquito noise because the graphic was defined by math. It was rendering a grey background and two circles. (I deleted the grey background.) The outer circle contained the notch, and the inner circle defined the white space. I was now hooked and dreamed about a pure SharePoint 2013 Master Page defined by SVG.

I had my SVG, now I needed to program the darn thing. I found an SVG JavaScript library called Snap SVG. It was created by Adobe and is a JS helper to create complex SVG XML markup. SVG elements can adhere to a variety of browser events, user interactions, animations, twists, turns, zooms, skews. You need it, it will do it. I needed it to do the following: click handling, hover effects and some kind of percentage-based gradient control.

Enough yapping, I suppose.. You want the script, right?  I wrote this in a Content Editor Web Part, but it would work much better in a Page Layout. The styles and script go in the “PlaceHolderAdditionalPageHead” section, and the HTML/SVG markup would go where ever you want it in the content area. (“PlaceHolderMain”).

<style type="text/css">

  .ratings-load-msg {

    position: absolute;

    left: 16px;

  }

  .ratings-wait-msg {

    position: absolute;

    left: 16px;

    display: none;

  }

  

  .sp-gears {

    position: relative;

    top: 8px;

  }

  

</style>



<script src="//code.jquery.com/jquery-1.7.2.min.js" type="text/javascript">

</script>

<script src="/Style Library/snap.svg-min.js"type="text/javascript">

</script>

<script type="text/javascript" src="/_layouts/15/sp.runtime.js">

</script>

<script type="text/javascript" src="/_layouts/15/sp.js">

</script>

<script type="text/javascript" src="/_layouts/15/Reputation.js">

</script>

<script type="text/javascript">

  

  var Ratings = {

  };

  

  Ratings.Reader = (function ($) {

    var pub = {

    };

  

  pub.Init = function () {

    $('.ratings-wait-msg').hide();

    

 	$('.ratings-load-msg').show();

      

      //get the current users ratings for this page

      var pageId = _spPageContextInfo.pageItemId;

      

      var ctx = new SP.ClientContext.get_current();

      var pagesList = ctx.get_web().get_lists().getByTitle('Pages');

      var li = pagesList.getItemById(pageId);

      

      ctx.load(li, "AverageRating");

      ctx.executeQueryAsync(function (s, e) {

	    $('.ratings-load-msg').hide();

      var avgRating = parseFloat(li.get_item("AverageRating"));

      

      if(!avgRating)

        avgRating = 0;

      

      //defines SVG circle paths..

      var outerCircle = "M8.6 14.5L4.1 17.2C4.48996 19.0654 5.54326 20.9351 6.89498 22.2714C8.17013 23.5319 9.60781 24.4326 11.3 25.0213C12.4493 25.4211 13.5956 25.5 14.8 25.5C24.1656 25.5 29.197 14.0203 22.8925 7.10008C21.9869 6.10607 20.8468 5.19277 19.6 4.6615C17.6027 3.81039 15.53 3.40572 13.4 3.61698C10.0562 3.94057 6.74251 6.1291 5.17454 9.1C4.85677 9.70209 3.93163 11.0509 4.17076 11.7671C4.29506 12.1395 4.98439 12.4188 5.3 12.6078L8.6 14.5z";

      var innerCircle = "M14.4 8.64244C13.2754 8.78829 12.3584 8.92856 11.4 9.60432C7.8376 12.1162 8.07516 17.6363 11.9 19.7956C13.1061 20.4765 14.4632 20.5509 15.8 20.3844C16.5291 20.2936 17.1672 20.0225 17.8 19.6572C22.9208 16.7013 20.5541 7.84429 14.4 8.64244z";

      

      var s = Snap("#ratingsWrapper");

      var ratingMsg = "Average Rating: " + avgRating;

      var msg = s.text(25, 50, ratingMsg);

      

      for (var i = 0; i < 5; i++) {

		var x = (i + 1) * 27.5;

          var gradLine = 0;

          

          var ratingsRemainder = avgRating - i;

          if (ratingsRemainder > 0)

            gradLine = ratingsRemainder * 100;

          if (ratingsRemainder > 1)

            gradLine = 100;

          

          /* 50 = Blue to White center gradient line (three values at 50)

// Dont's ask me why there needs to be three, but they all need the same value

//to produce a solid vertical line

var g = s.gradient("l(0, 0, 1, 0)#0000FF:50-#fff:50-#fff:50-#fff");

*/

          

          var grad = s.gradient("l(0, 0, 1, 0)#ec602f:" + gradLine + "-#fff:" + gradLine + "-#fff:" + gradLine + "-#fff");

          

          var wrap1 = s.svg().attr({ x: x, y: '10' });

          var grp = wrap1.group();

          

          var rp1 = wrap1.path(outerCircle).attr({

            //fill: '#ec602f',

            fill: grad,

            stroke: "#ec602f",

            strokeWidth: 1,

            g: grp

          }).data("i", i + 1)

              .click(function () {

                $('#ratingsWrapper svg').remove();

                $('#ratingsWrapper text').remove();						

                $('.ratings-wait-msg').show();

                Ratings.Reader.Poster(this.data("i"));

              })

              .hover(

                function () {

                  msg.attr({ text: "Click Logos to Rate" });

                  this.attr({ stroke: "#e9e6e2" });

                },

                function () {

                  this.attr({ stroke: "#ec602f" });

                }

              );

          

          var rp1i = wrap1.path(innerCircle).attr({

            fill: '#fff',

            stroke: "#ec602f",

            strokeWidth: 1,

            g: grp

          }).data("i", i + 1)

              .click(function () {

                $('#ratingsWrapper svg').remove();

                $('#ratingsWrapper text').remove();

                $('.ratings-wait-msg').show();

                Ratings.Reader.Poster(this.data("i"));

              })

              .hover(

                function () {

                  msg.attr({ text: "Click Logos to Rate" });

                  this.attr({ stroke: "#e9e6e2" });

                },

                function () {

                  this.attr({ stroke: "#ec602f" });

                }

              );

        }

    },

                          function (s, e) {

                            alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());

                          });

    }

      

      pub.Poster = function(rating) {

        //alert(rating);

        var listId = _spPageContextInfo.pageListId;

        var itemId = _spPageContextInfo.pageItemId;

      

      EnsureScriptFunc('reputation.js', 'Microsoft.Office.Server.ReputationModel.Reputation',

                       function() {

                         var ctx = new SP.ClientContext();

                         rating =

                           Microsoft.Office.Server.ReputationModel.Reputation.setRating(ctx, listId, itemId , rating);

                         ctx.executeQueryAsync(done, fail);

                       });

      

      function done() {

		alert("Done");

          $('#ratingsWrapper').show();

          $('.ratings-load-msg').hide();

          

          Ratings.Reader.Init();

    	}

      

      function fail() {

	    alert("Failed");

          // If it fails for whatever reason, dump it in the console for debugging,

          // or tell the user

        }	

    }   	

      

      return pub;

}(jQuery));

  

  _spBodyOnLoadFunctionNames.push('Ratings.Reader.Init');

</script>



<div>

  <div class="ratings-load-msg">

    <img src="/_layouts/images/gears_anv4.gif" class="sp-gears" />

    Loading Ratings...

  </div>

  <div class="ratings-wait-msg">

    <img src="/_layouts/images/gears_anv4.gif" class="sp-gears" />

    Setting Your Rating...

  </div>

  <svg id="ratingsWrapper" height="1500" width="400">

  </svg>

</div>

Notes:

Pardon the omission of line numbers in the snippet above. We're working on fixing that. These will obviously make sense when you copy the snippet into a program with line numbers.

Lines 18-22: Loading the required scripts.

Lines 35-41: This is my SharePoint ratings getter. This script was intended to be run from a Publishing Page. Many of the variables I used are derived from _spPageContextInfo.

Lines 49-50: These are the string definitions of the circle paths. Note that I divided each number by 10, since the image was too big.

Lines 56-57: For loop iterates 5 times, and the x variable helps to separate the “stars” along the horizontal axis.

Lines 60-72: A gradient is defined based on the decimal value of the ratings average for each each “star”. This allows for a sharp vertical line that precisely describes the percentage of the average rating with a graphical representation. (Out-of-the-box, the ratings stars only have a .50 precision.) 

Lines 74-120: The circles are created, grouped, positioned in the 2D plane. Each has a specific hover and click event attached. The double hover event shows a cool animated effect on mouse over.

Lines 128-154: The ratings posting function uses the Reputation.setRating function from SharePoint’s reputation JS.

 

Here’s what it looks like (ironically a PNG):

custom-star-ratings-2013

I also have a non-SharePoint-y version up on CodePen:  http://codepen.io/thewef/pen/MYJVxy