Tags

, ,

There are a lot of SharePoint tooltips plugins, but I am not aware of anyone close to SharePoint environment. So, I said, why not try to create one and this is how I created code below. It is presented for didactic purpose, but I have a plan to extend.

First thing we need to do is to create a communication between the tooltip callout and SharePoint, because it is almost sure we will need to get the data dynamically from SharePoint lists.

Shp.Data = function () {
    /// <summary>SharePoint Data static class</summary>
}

Shp.Data.getListItems = function (getListItemsOptions, success, fail) {
    /// <summary>Get items from SharePoint using client object model</summary>
    /// <param name="getListItemsOptions" mayBeNull="false" optional="false" type="Object"></summary>
    /// <param name="success" mayBeNull="false" optional="false" type="Function"></summary>
    /// <param name="fail" mayBeNull="false" optional="true" type="Function"></summary>
    var e = Function.validateParameters(arguments, [{ name: 'getListItemsOptions', mayBeNull: false, optional: false, type: Object },
                                                    { name: 'success', mayBeNull: false, optional: false, type: Function },
                                                    { name: 'fail', mayBeNull: false, optional: true, type: Function }], true);
    if (e) throw 'Shp.DOM.getListItems was called with invalid paramater.';

    var caml = caml || '<View></View>';
    var fail = fail || function (sender, args) {
        alert('Error getting data from ' + getListItemsOptions[listName]);
    }
    var ctx = getListItemsOptions['context'] || SP.ClientContext.get_current();
    var oList = ctx.get_web().get_lists().getByTitle(getListItemsOptions['listName']);
    var camlQuery = new SP.CamlQuery();
    camlQuery.set_viewXml(caml);
    var oListItems = oList.getItems(camlQuery);
    ctx.load(oListItems);
    ctx.executeQueryAsync(function () {
        success(oListItems);
    }, fail);
}

Shp.Data.registerClass('Shp.Data');

The second step is to create code for callout. Shp.Data will be used to connect to the SharePoint and Shp.DOM will present it into tooltips.

/// <Reference Name="MicrosoftAjax.js" />
/// <Reference Name="MicrosoftAjaxWebForms.js" />
Type.registerNamespace('Shp');

Shp.DOM = function () {
    /// <summary>SharePoint DOM static class</summary>
}

Shp.DOM.addCallout = function (el, calloutOptions) {
    /// <summary>Addd callout for an element</summary>
    /// <param name="el" mayBeNull="false" optional="false" domElement="true"></summary>
    /// <param name="calloutOptions" mayBeNull="false" optional="false" type="Object"></summary>
    var e = Function.validateParameters(arguments, [{ name: 'el', mayBeNull: false, optional: false, domElement: true },
                                                    { name: 'calloutOptions', mayBeNull: false, optional: false, type: Object }], true);
    if (e) throw 'Shp.DOM.addCallout was called with invalid paramaters.';

	// build variable required for tooltip creation
    var html = calloutOptions['html'] || el.title;
    var position = calloutOptions['position'] || 'right';
    var sharepoint = calloutOptions['sharepoint'] || null;
    var callout = callout || document.createElement('DIV');
    var timer = null;

	// add loader image if data is retrieved from SharePoint
    var addLoaderImage = function () {
        var img = '<div class="callout-loader-container">' +
    			   '<img class="callout-loader" src="https://hp314.sharepoint.com/sites/sprint/SitePages/ajax-loader.gif" />' +
    			   '</div>';
        callout.innerHTML = img;
    }

	// show tooltip callout
    var showCallout = function () {

        if (timer !== null) {
            clearTimeout(timer);
        }
        callout.innerHTML = html;
        callout.style.position = 'absolute';
        callout.className = 'callout ' + position;
        if (sharepoint !== null) {
            addLoaderImage();
        }
        document.body.appendChild(callout);

        // Set the position of the callout
        var top;
        var left;
        switch (position) {
            case 'right':
                top = Shp.DOM.asbTop(el) - 25 - Math.floor(callout.offsetHeight / 2) + Math.floor(el.offsetHeight / 2);
                left = Shp.DOM.asbLeft(el) + el.offsetWidth - 10;
                break;
            case 'left':
                top = Shp.DOM.asbTop(el) - 25 - Math.floor(callout.offsetHeight / 2) + Math.floor(el.offsetHeight / 2);
                left = Shp.DOM.asbLeft(el) - callout.offsetWidth - 40;
                break;
            case 'top':
                top = Shp.DOM.asbTop(el) - 25 - callout.offsetHeight - 10;
                left = Shp.DOM.asbLeft(el) - 25 - Math.floor(callout.offsetWidth / 2) + Math.floor(el.offsetWidth / 2);
                break;
            case 'bottom':
                top = Shp.DOM.asbTop(el) + 10;
                left = Shp.DOM.asbLeft(el) - 25 - Math.floor(callout.offsetWidth / 2) + Math.floor(el.offsetWidth / 2);
                break;
        }

        callout.style.top = top + 'px';
        callout.style.left = left + 'px';

        // If sharepoint option is specified get data from sharepoint
        if (sharepoint !== null) {
            Shp.Data.getListItems({ listName: sharepoint['listName'] },
            	// Items are get from server (succes)
	            function (items) {
	                if (timer == null) {
	                    timer = setTimeout(hideCallout, 1000);
	                }
	                var calloutHtmlProvider = sharepoint['markupFactory'];
	                callout.innerHTML = calloutHtmlProvider(items, callout);
	            },
	            // Items are not retrieved from server (error)
	            function(sender, args) {
	            	callout.innerHTML = 'Error getting data from ' + sharepoint['listName'] +
	            						'<br />' + sender.get_message();
	            });
        }

    }

    var hideCallout = function () {
        try {
            document.body.removeChild(callout);
        }
        catch (e) {
        }
    }

    Sys.UI.DomEvent.addHandler(callout, 'mouseover', function () {
        if (timer !== null) {
            clearTimeout(timer);
        }
    });

    Sys.UI.DomEvent.addHandler(callout, 'mouseout', function () {
        timer = setTimeout(hideCallout, 1000);
    });

    Sys.UI.DomEvent.addHandler(el, 'mouseover', showCallout);
    Sys.UI.DomEvent.addHandler(el, 'mouseout', function () {
        timer = setTimeout(hideCallout, 1000);
    });

}

Shp.DOM._getPos = function (el, prop) {
    /// <summary>Get position from left or top an element (internal use)</summary>
    /// <param name="el" mayBeNull="false" optional="false" domElement="true"></param>
    /// <param name="prop" mayBeNull="false" optional="false" type="String"></param>
    var c = el[prop];
    var parent = el.offsetParent;
    while (parent !== null && parent.tagName != 'BODY') {
        c += parent[prop];
        parent = parent.offsetParent;
    }
    if (parent !== null) {
        c += parent[prop];
    }
    return c;
}

Shp.DOM.asbLeft = function (el) {
    /// <summary>Get position from left of an element</summary>
    /// <param name="el" mayBeNull="false" optional="false" domElement="true"></param>
    var e = Function.validateParameters(arguments, [{ name: 'el', domElement: true, mayBeNull: false, optional: false }], false);
    if (e) throw 'Shp.DOM.asbLeft was called with invalid parameter';
    return Shp.DOM._getPos(el, "offsetLeft") - Shp.DOM._getPos(el, "scrollLeft");

}

Shp.DOM.asbTop = function (el) {
    /// <summary>Get position from top of an element</summary>
    /// <param name="el" mayBeNull="false" optional="false" domElement="true"></param>
    var e = Function.validateParameters(arguments, [{ name: 'el', domElement: true, mayBeNull: false, optional: false }], false);
    if (e) throw 'Shp.DOM.asbTop was called with invalid parameter';
    return Shp.DOM._getPos(el, "offsetTop") - Shp.DOM._getPos(el, "scrollTop");
}

Shp.DOM.getInnerText = function (el) {
    /// <summary>Get innerText from a DOM element, if property is available</summary>
    /// <param name="el" mayBeNull="false" optional="false" domElement="true"></param>
    /// <returns type="String"></returns>
    var e = Function.validateParameters(arguments, [{ name: 'el', domElement: true, mayBeNull: false, optional: false }], false);
    if (e) throw 'Shp.DOM.getInnerText was called with invalid parameter';

    // Try returning textContent (applicable for Mozilla)
    if (typeof el.textContent !== 'undefined' && el.textContent !== null) {
        return el.textContent;
    }

    // Try returning innerText (applicable for Chrome and IE)
    if (typeof el.innerText != 'undefined') {
        return el.innerText;
    }

    return undefined;
}
Shp.DOM.registerClass('Shp.DOM');

The last thing is to include some CSS into the page. This is not created by me, but you can find it here

And now, let me explain how to use it. The simple way is to create the callout is this:

	var element = $get('elementId');
	Shp.DOM.addCallout(element ,{ position: 'top' });

Content of the callout box will be taken from title property of the element. Also, position property can have following values: top, left, right, bottom. But if you can to specify your own html, you can do this easily.

	var element = $get('elementId');
	Shp.DOM.addCallout(element ,{ position: 'top', html: 'callout html content goes here' });

The last scenario is to get content of callout from a SharePoint list. For this, you need to specify a new property called “sharepoint”, which is actually an object containing these properties.

listName (required) Name of the SharePoint list
markupFactory (required) A function which accepts obtained list items and DOM element to which callout is attached. It should return a string which will become callout content.
caml (optional) CAML query string.

More concrete, here is how you can use it.

var element = $get('elementId');
var opt = {
    listName: 'test',
    markupFactory: function (items, callout) {
        var html = '';
        for (var i = 0; i < items.get_count() ; i++) {
            html += items.itemAt(i).get_item('Title');
            html += '<br />';
        }
        return html;
    }
}

Shp.DOM.addCallout(otherElement, {
    position: 'top',
    sharepoint: opt
});

Code is still under development, but you can have an idea about how it is supposed to work. If you want to contribute to it, you are most welcome!

Creating tooltips in a view (a practical implementation)

For this example all I needed is to create a list containing the default column Title, which will serve as trigger element for our tooltips, and Description (multiple lines of text) which will contain callouts content.

1. Adding required files into the page.

Open the master in page with SharePoint Designer and add required JavaScript files using asp:ScriptManager.

<asp:ScriptManager id="ScriptManager" runat="server" EnablePageMethods="false" EnablePartialRendering="true" EnableScriptGlobalization="false" EnableScriptLocalization="true" LoadScriptsBeforeUI="false">
	<Scripts>
	<asp:ScriptReference Path="../../Style Library/Sprint/js/sharepoint.data.js" />
	<asp:ScriptReference Path="../../Style Library/Sprint/js/sharepoint.dom.js" />
	</Scripts>
</asp:ScriptManager>

Add also reference to callout CSS file.

<SharePoint:CssRegistration Name="/sites/sprint/Style Library/Sprint/css/callout.css" runat="server" EnableCssTheming="false" />

2. Make required settings in the view.

Go to the “All Items” view or any other if you prefer so, edit the page, edit the webpart and tick “Server Render” checkbox. In the same time in “XSL Link” add a link to a XSL with the following content.

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" xmlns:asp="http://schemas.microsoft.com/ASPNET/20" xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:SharePoint="Microsoft.SharePoint.WebControls" xmlns:ddwrt2="urn:frontpage:internal" xmlns:o="urn:schemas-microsoft-com:office:office">
<xsl:include href="/_layouts/xsl/main.xsl"/>
<xsl:include href="/_layouts/xsl/internal.xsl"/>
 
<xsl:template name="ShowToolip" ddwrt:dvt_mode="body" match="FieldRef[@Name='Title']" mode="Text_body" ddwrt:ghost="show">
<xsl:param name="thisNode" select="." />
<xsl:param name="TooltipElement" select="$thisNode/@Title" />
<xsl:param name="TooltipContent" select="$thisNode/@Description" />
<xsl:if test="$TooltipElement != ''">
    <a title="{$TooltipContent}" href="#" class="jstooltip"><xsl:value-of select="$TooltipElement" disable-output-escaping="yes" /></a>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

2. Create tooltips.

Add into your page view a script editor control. This should contain the following code.

function pageLoad (sender, args) {
	var elements = document.querySelectorAll('a.jstooltip');
	for(var i = 0; i &lt; elements.length; i++) {
		Shp.DOM.addCallout( elements[i],{ position: 'right' });
	}
}

You can use jQuery.document(ready) or any other way to attach a method to page load event. I used MicrosoftAjax specific way because is trigger for complete postback as well as for partial postback. Content of description column should appear also in callout content for the Title (be sure you included in the view Title column, not TitleLink).

For more details, if you have a SharePoint Online account, drop me a message to share the page with you.

2014-10-04 09_53_52-Tooltips - All Items

Advertisements