SPTechCon Social Demo
[important]
Mark Miller (@EUSP) and I did a webinar on Friday, March 15, 2013 at to show the demo again and take questions about how we built it. If you’d like to look at the demo yourself, check out the publicly available page on NothingButSharePoint.com. The video is embedded below.
[/important]
It’s not every day that I get to create something in SharePoint with a Magic button.
I few weeks ago, Mark Miller pinged me asking if I’d be interested in helping him with a demo for his keynote at SPTechCon. When Mark asks, I tend to ask “how high” simply because it’s usually something fun. Sometimes, it’s even useful.
The basic idea was to show a social dashboard in SharePoint without letting on that we were using SharePoint. As you might imagine, none of this went along as linearly as I’m going to describe it. We tried various different things, added and removed columns, and widgets, etc. What I’ll describe is the end result.
First, I knew I wanted to use jQuery, jQueryUI, and SPServices, so I took a copy of default.aspx in a basic Team Site and added the following references. (Yes, I prefer the old Web Part Page over the Wiki pages. It’s much easier to work with. The Wiki pages tend to vomit markup all over what I do.)
<link href="Scripts/social_home_page.css" rel="stylesheet" type="text/css" /> <!-- Reference the jQueryUI theme's stylesheet on the Google CDN. Here we're using the "Start" theme --> <link href="/ajax/libs/jqueryui/1.10.0/themes/start/jquery-ui.css" rel="stylesheet" type="text/css" /> <!-- Reference jQuery on the Google CDN --> <script type="text/javascript" src="/ajax/libs/jquery/1.8.3/jquery.min.js"></script> <!-- Reference jQueryUI on the Google CDN -- > <script type="text/javascript" src="/ajax/libs/jqueryui/1.10.0/jquery-ui.min.js"></script> <!-- Reference SPServices on cdnjs (Cloudflare) --> <script type="text/javascript" src="/ajax/libs/jquery.SPServices/0.7.2/jquery.SPServices-0.7.2.min.js"></script> <!--<script type="text/javascript" src="http://cdn.dev.skype.com/uri/skype-uri.js"></script>--> <script type="text/javascript" src="Scripts/social_home_page.js"></script>
Next, I altered the markup for the page in SharePoint Designer. This was a demo, so there was zero concern about repeatability, of course. It’s nice to have that sort of freedom once in a while. By default, the page has a table with two columns set to 70% (left) and 30% (right) width. I simply changed that markup to have four columns, each with 25% width. In each of the two new table columns, I added a Web Part Zone. I wanted to drop as much of the social stuff as I could into the page using Content Editor Web Parts so that I could explain it more easily, like in this blog post. In edit mode, the page looked something like this: Next I added some CSS to the social_home_page.css file:
#s4-ribbonrow { display:none; } .s4-titlerowhidetitle { display:none; } #s4-leftpanel { display:none; } #MSO_ContentTable { margin-left:0; } #MSO_ContentTable table td { padding:3px; }
This simple CSS removed all of the chrome we normally see on the page. In other words, we’ve removed the SharePoint from SharePoint to a large degree. The ribbon, title area, and Quick Launch are all totally hidden. (No screen shot for this – you wouldn’t see anything!) At this point, I had a page that looked like a big white nothing in the browser. Time to add the fun stuff. In Twitter, you can create widgets that display tweets from Twitter much like you can create different streams in HootSuite or TweetDeck. You can find this capability in Settings: I created three different widgets:
- search stream for “EUSP”
- search stream for “SPTechCon”, and
- search stream for “SharePoint”
There are a few settings which give you basic control over the look and feel. If we were implementing this “for real”, it would probably make sense to make calls against the Twitter API and get the raw data so that we would have full control over the markup and visuals. When you create the widget, you get a little snippet of markup and script which you can embed in a Web page. Those snippets are very simple. Here’s what the snippet for EUSP looks like:
<a class="twitter-timeline" href="https://twitter.com/search?q=EUSP" data-widget-id="306449761781817344"></a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
I put each snippet into a TXT file so that I could reference them in CEWPs. For the EUSP stream, I have a TXT file called twitter_search_eusp.txt in a Document library, and I just point to that file in the CEWP’s Content Link. This gets the three Twitter streams into the page:
We also wanted to show something from Yammer, since it plays so importantly into Microsoft’s social story. While there are Yammer Web Parts available, I decided to go the widget route here as well. If you look under Apps in the Yammer Web interface, you’ll find Yammer Embed, which eventually takes you to the Yammer Embed page. There, you’ll find a PDF document that explains how to embed a Yammer feed. (Note to the Yammer folks: This isn’t a very clear process. Opportunity for improvement.)
Here’s what I ended up with for the SPYam feed. Note that I created my own id for the container so that it would fit into my page logic easily.
<script data-app-id="hyB2pTvrL36Y50py8EWj6A" src="https://assets.yammer.com/platform/yam.js"></script> <script> yam.connect.embedFeed({ container: '#yammer-spyam-embedded-feed', network: 'spyam' // update with your own }); </script> <div id="yammer-spyam-embedded-feed"></div>
For some reason, the Yammer authentication mechanism simply won’t work in my page in Internet Explorer 9, so I switched to Firefox at this point, where I had no problems. (Another place for improvement, Yammer folks. However, it may well be that my IE9 is just sick.)
Now we have all four streams we want in the page, at least in Firefox, for me.
I had seen some Twitter traffic about Dan McPherson’s lastest entries in the SharePoint Games under the Leap moniker. There are two offerings right now: LeapBackup and LeapMessages. Mark and I thought it would be cool if we could use LeapMessages to send a text message with the contents of a Tweet. I can imagine several use cases for this. Perhaps you work at Burger King, your Twitter account is hacked, and you need to notify the execs…
LeapMessages is really simple to implement. You just sign up on the Leap site, give the site URL and your Office365 credentials (it only works on Office365 right now), and Leap creates a list in your site called LeapMessages. If you add an item to that list, the LeapMessages engine grabs it (it polls every 30 seconds) and sends it out as a text from their gateway. Easy-peasy.
To implement this, I pulled a demo cheat. Since it takes a little time to load up the three Twitter streams and the Yammer stream, I set it up so that when I hovered over the Magic button (see below for more info on the button), this script would run, adding an SMS link at the bottom of each tweet in the three streams. I just had to wait until the three Twitter streams were loaded before I did the hover trick.
$("div.showSharePoint").hover(function () { if (!loadedSMS) { loadedSMS = true; $("iframe[id^='twitter-widget-']").contents().find("ul.tweet-actions").prepend("<ul><li><a class="SMS-send">SMS</a></li></ul>"); $("iframe[id^='twitter-widget-']").contents().find(".SMS-send").click(function (e) { e.preventDefault(); var thisTweet = $(this).closest("div.footer").closest("li"); var thisTweetText = thisTweet.find("div.e-entry-content").text().trim(); var thisTweetFullName = thisTweet.find("div.header span.full-name").text().trim(); var thisTweetNickName = thisTweet.find("div.header span.p-nickname").text().trim(); messageText.val("Interesting tweet from " + thisTweetFullName + " (" + thisTweetNickName + ") " + thisTweetText); countChars(); $("#dialog").dialog("open"); }); } }, function () { // Nothing to do });
I added this markup at the bottom of the page for the dialog that would pop up when I clicked the SMS links.
<div id="dialog" title="Send SMS Message"> <p class="validateTips">All fields are required.</p> <head> <meta name="WebPartPageExpansion" content="full" /> </head> <form> <fieldset> <label for="phoneNumber">Phone Number</label>+1 <input type="text" name="phoneNumber" id="phoneNumber" class="text ui-widget-content ui-corner-all" /> <br /> <label for="messageText">Message</label> <textarea rows="5" cols="40" name="messageText" id="messageText" value="" class="text ui-widget-content ui-corner-all"></textarea> <div id="charCount"></div></fieldset> </form> </div>
When I clicked the SMS link, this script drove the dialog and wrote the entered data into the LeapMessages list using SPServices.
dialogDiv.dialog({ autoOpen : false, height : 300, width : 500, modal : true, buttons : { "Send Message" : function () { var bValid = true; allFields.removeClass("ui-state-error"); bValid = bValid && checkLength(phoneNumber, "Phone number", 10); bValid = bValid && checkRegexp(phoneNumber, /^([0-9])+$/i, "Phone number can only contain digits."); // From jquery.validate.js (by joern), contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ if (bValid) { // Send the SMS message $().SPServices({ operation : "UpdateListItems", async : false, batchCmd : "New", listName : "LeapMessages", valuepairs : [ ["ReceiverMobileNumber", "+1" + phoneNumber.val()], ["Message", encodeXml(messageText.val())] ], completefunc : function (xData, Status) {} }); $(this).dialog("close"); } }, Cancel : function () { $(this).dialog("close"); } }, close : function () { allFields.val("").removeClass("ui-state-error"); } });
There are a few helper functions referenced above, and here they are:
var loadedSMS = false; var dialogDiv = $("#dialog"); var phoneNumber = $("#phoneNumber"), messageText = $("#messageText"), allFields = $([]).add(phoneNumber).add(messageText), tips = $(".validateTips"); function checkLength(o, n, num) { if (o.val().length > num || o.val().length < num) { o.addClass("ui-state-error"); updateTips(n + " must be " + num + " digits."); return false; } else { return true; } } function checkRegexp(o, regexp, n) { if (!(regexp.test(o.val()))) { o.addClass("ui-state-error"); updateTips(n); return false; } else { return true; } } function updateTips(t) { tips.text(t).addClass("ui-state-highlight"); setTimeout(function () { tips.removeClass("ui-state-highlight", 1500); }, 500); } function countChars() { var messageLen = messageText.val().length; var errorClass = messageLen > 160 ? "class='ui-state-error'" : ""; $("#charCount").html("You have used <span " + errorClass + " >" + messageLen + "</span> of 160 characters"); } messageText.keyup(function () { countChars(); });
I decided to add one last piece of functionality. I couldn’t figure out how frequent the Twitter polling was, but when there was a new tweet in any any of the streams, I wanted to highlight it. To do this, I added this bit of script. It ran every three seconds to see if anything new had shown up, and if it had, it highlighted the counter at the top of each Twitter stream.
// Show tweet counters setInterval("countTweets()", 3000); function countTweets() { $("iframe[id^='twitter-widget-']").each(function () { var tweetCount = $(this).contents().find("li.tweet").length; var thisCounter = $(this).prev(".tweet-counter"); if (thisCounter.length === 1) { var currCount = thisCounter.html(); if (tweetCount != currCount) { thisCounter.addClass("ui-state-highlight"); setTimeout(function () { thisCounter.removeClass("ui-state-highlight", 1500); }, 10000); } thisCounter.html(tweetCount); } else { $(this).before("<div class="tweet-counter">" + tweetCount + "</div>"); } }); }
The Magic button is just a div. Yes, I violated best practices by making it a div instead of a real button. Don’t try this at home.
<div class="showSharePoint">Magic</div>
The Magic button contains the reveal. When I clicked on it, this script ran, unveiling the fact that the page was, in fact, a SharePoint page. There were very few oohs or aaahs that I could hear, which was a little disappointing, but hey, maybe I pushed it too fast or something.
// Set up the Magic button $("div.showSharePoint").click(function () { var thisValue = $(this).text(); if (thisValue == "Magic") { $("#RibbonContainer").hide(); $("#s4-ribbonrow").slideDown(5000); $("#RibbonContainer").slideDown(5000, function () { $(this).fadeIn(1000) }); $(".s4-titlerowhidetitle").fadeIn(5000); $("#s4-leftpanel").fadeIn(5000); $("#MSO_ContentTable").animate({ "margin-left" : "150px" }, 5000, function () { // animation complete }); $(this).text("No Magic"); } else { $("#RibbonContainer").slideUp(5000, function () { $(this).fadeOut(1000) }); $("#s4-ribbonrow").slideUp(5000); $(".s4-titlerowhidetitle").slideUp(5000); $("#s4-leftpanel").hide(); $("#MSO_ContentTable").animate({ "margin-left" : "0px" }, 5000, function () { // animation complete }); $(this).text("Magic"); } });
So there you have it. I’m sure I’ve left some bits and bobs out of this in trying to make it consumable here, but I think I’ve gotten most of the important parts. All in good fun, and hopefully it got the message across that SharePoint doesn’t have to look like SharePoint and that we can do useful social stuff in SharePoint in many cool ways.
I’ve thought of a few neat things we could do with this, like storing the snippets in a SharePoint list and letting the user select which ones they want to see in their version of the page, dragging and dropping the streams into the order they like, and saving their settings for when they return. The possibilities with this stuff are really endless, depending on what you want to accomplish.
Have fun!