SharePoint - stripping HTML tags in XSL

Sometimes, when working with XSL (for example, in a content query webpart), you would like to limit the number of characters returned in a field in the template. 

Trimming field length in XSL

The itemstyle template may look something like this:

    <xsl:template name="FAQ" match="Row[@Style='FAQ']" mode="itemstyle">       
        <xsl:variable name="SafeLinkUrl">
            <xsl:call-template name="OuterTemplate.GetSafeLink">
                <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
            </xsl:call-template>
        </xsl:variable>

        <dt>
            <a href="{$SafeLinkUrl}" class="title" onclick="GoToLink(this);return false;" target="_self">
                <xsl:value-of select="@Title" />
            </a>
        </dt>
        <dd>
            <xsl:choose>
                <xsl:when test="string-length(@Answer) &gt; 150">
                    <xsl:value-of select="concat(substring(@Answer,0,150),'...')" disable-output-escaping="yes"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="@Answer" disable-output-escaping="yes"/>
                </xsl:otherwise>
        </xsl:choose>
       </dd>
    </xsl:template>

The interesting part is where the @Answer field is rendered - if the length of this field is beyond 150 characters, it will return the first 150 characters and append an ellipsis (...)

Problem when we have a HTML field

The technique is pretty simple, but it is not fool proof - when the field you are trying to trim is an HTML / Rich field, you have a big problem - the trimming may suddenly cut off valid HTML, to return an invalid HTML.

Imagine, if the @Answer field was:

<div>
     <span>a great answer</span>
</div>

And we perform:

substring(@Answer, 25)

Then we'll get:

<div>
     <span>a great answe
r</span>
</div>

 

You are now returning really bad HTML to the browser:

<dd>
    <div>
        <span>a great answe
...
</dd>

See how the HTML isn't terminated correctly with the proper end-tags.  If you are lucky, the browser guesses correctly and doesn't break your page.  Many times though, it's all down hill from here.

 

Using XSL to strip HTML tags from your field.

A very simple solution is to add an additional XSL template to strip out the HTML tags from your field before rendering it.  A simple version can be found on this article.

 

1.  Add this removeHtmlTags template in your XSL

<xsl:template name="removeHtmlTags">
    <xsl:param name="html"/>
    <xsl:choose>
      <xsl:when test="contains($html, '&lt;')">
        <xsl:value-of select="substring-before($html, '&lt;')"/>
        <!-- Recurse through HTML -->
        <xsl:call-template name="removeHtmlTags">
          <xsl:with-param name="html" select="substring-after($html, '&gt;')"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$html"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

 

2. Modify your ItemStyle to use the removeHtmlTags template in an XSL variable

    <xsl:template name="FAQ" match="Row[@Style='FAQ']" mode="itemstyle">       
        <xsl:variable name="SafeLinkUrl">
            <xsl:call-template name="OuterTemplate.GetSafeLink">
                <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
            </xsl:call-template>
        </xsl:variable>
        <xsl:variable name="textAnswer">
            <xsl:call-template name="removeHtmlTags">
                <xsl:with-param name="html" select="@Answer" />
            </xsl:call-template>
        </xsl:variable>

        <dt>
            <a href="{$SafeLinkUrl}" class="title" onclick="GoToLink(this);return false;" target="_self">
                <xsl:value-of select="@Title" />
            </a>
        </dt>

        <dd>
            <xsl:choose>
                <xsl:when test="string-length($textAnswer) &gt; 150">
                    <xsl:value-of select="concat(substring($textAnswer,0,150),'...')" disable-output-escaping="yes"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$textAnswer" disable-output-escaping="yes"/>
                </xsl:otherwise>
            </xsl:choose>
       </dd>
    </xsl:template>

Summary

  1. Use substring to strip fields to a certain number of characters - so you don't return everything
  2. For HTML fields, add removeHtmlTags template and use call-template to get the result into an XSL variable first

SPSPerth 2012 Update

This is my first time in Perth!  Hello Perthites!

I even took a train to Cottesloe beach - a very nice beach, I watched the sunset and did what any geek would do, I checked in the Indian Ocean.

I will update this post with links after the sessions. In the mean time, you can reference my current collection of articles on these topics regarding REST, custom service, and Knockout

/rest/

Downloads

Reference

Make sure you follow Matt Menezes
http://spmatt.wordpress.com/

He's a regular at Perth user group circles and knows a lot about Knockout.  I think he is slotted in for a future session in the upcoming months - don't miss it!

SharePoint - Recursive delete SPWeb PowerShell script

Or, what happens when you stuffed up and need to clean up a lot of sites created by accident.

 

Story

  • I have a feature receiver stapled to my site template, it checks a few conditions, does a few things, then creates 1 subsite and stops
  • The new subsite is created, and runs the same receiver, it checks a few more conditions, does a few things.  It shouldn't create any subsites.
  • This afternoon, it did.  The process did not stop.
  • Ouch

 

Problem

  • You can't delete the higher level web object when it has subsites.  You can't follow the subsites because the path has gone way deep.
  • Sounds like time for a good PowerShell script

 

Script

$web = get-spweb http://dev/ourprojects/nsw/1000

function CleanSite( $w )
{
    $ws = $w.Webs;
    foreach( $w1 in $ws)
    {
        CleanSite($w1)
    }
    echo $w.Title
    $w.Delete()

}

CleanSite $web

 

Closing thoughts

My initial fears were that the feature receivers has gone berserk and I'd have hundreds of subsites to clean up.  This turned out to be not the case - I only had about 12 subsites.  I'm wondering if there was a SharePoint error that prevented deeper subsites from being created.  So the disaster really wasn't that bad.  12 subsites I can manage. 

image

InfoPath, custom WCF Service, Word and Open XML SDK (outline)

This upcoming series of articles promise to be far more exciting than the title.

Scenario

How do you use an InfoPath form document to populate a word document with content controls. 

 

Steps:

  1. Building a Word document template with Content Control place holders
  2. Use Content Control toolkit to bind values to place holders in the template to an embedded custom XML (hey, isn't an InfoPath document an XML file?)
  3. Creating a WCF service to take a Word template document, and replace the custom XML, by the power of Open XML SDK (2.0, of course).
  4. Invoking the WCF service from within an InfoPath document to generate a word template version of itself!
  5. Finally, a bit of discussion on where Word Automation services fits in the bigger picture, as well as thoughts on best practices.


Drop a comment below if you have any specific questions relating to these steps.  Specifically, if you can think of a far better title for this series.

Otherwise, stay tuned!  :-)

InfoPath - disabling backspace key in browser form

How to disable the backspace key using Javascript/jQuery for an InfoPath browser form in SharePoint 2010.

 

The problem

One really troubling problem with almost all web solutions is how the Backspace key works.  By default, it tells your browser to go back to the previous page in history.

If you are just browsing around on your Intranet, this is probably not a big deal - firstly, you wouldn't be pressing backspace unless you actually wanted to go back.  Secondly, if you did indeed made a mistake and pressed the backspace key, you'd just undo that action by clicking the forward button, or navigate to another link.  No problem.

 

When you are filling in a form on the browser though, such as through InfoPath, suddenly the backspace key is a big deal.  You users may be using the backspace key to delete text that they are in the middle of entering.  And if they didn't have a textbox focused, the backspace key is sent to the browser form, and suddenly you have a problem.  The form disappeared, and you have lost information.

 

InfoPath is quite smart - it remembers which view you are supposed to be on, and when you navigate to an outdated browser historical view of the form - Form Server will automatically redirect you forward to show you the correct form you are supposed to be viewing.  This is good, at least in the navigation sense.  So the only remaining problem is the lost data - your user may have had a whole page filled out and this mistake has just cleared their form.  Not good.

 

Enter Javascript

So, the plan is simple:

  1. Inject javascript to the existing browser form
  2. Listen to keydown event for a backspace key (keycode 8)
  3. Eat the event and stop it from propagation, so the browser don't see it

Using jQuery, you'll need this simple function.

 

function document_keydown(e) {
    if (e.keyCode == 8 && e.target.tagName != "INPUT") {

        // letting us know we've ate a backspace key
        SP.UI.Notify.addNotification('Ate a backspace key, hew!', false);

        // cancel backspace navigation
        e.preventDefault();
        e.stopImmediatePropagation();
        return false;
    }
};

$(document).keydown(document_keydown);

 

Injecting Javascript in modal dialog

 

If you are using SharePoint 2010's modal dialog to show InfoPath in a modal popup, it's slightly trickier.

 

// grab a reference to the modal window object in SharePoint
var w = SP.UI.ModalDialog.showModalDialog(options);

if (w) {
    // get the modal window's iFrame element
    var f = w.get_frameElement();

    // watch frame's readyState change - when page load is complete, re-attach keydown event
    // on the new document       
    f.onreadystatechange = function(e) {
        if (f.readyState == 'complete') {
            var fwin = f.contentWindow || f.contentDocument;
            $(fwin.document).keydown(document_keydown);
        }
    };
}

Result

 

nom-nom-nom backspace keys.

image

 

Note, because the Javascript catches the keypress event at the document level.  If your user still has focus on the input (textbox) level, the event will not be stopped - so your user still will be able to backspace when they are using a textbox.