Frederik Vig – ASP.NET developer

Follow me

Posts tagged ‘jQuery’

Backup plan when loading the jQuery library from CDN

In most of my project I load the jQuery library from a CDN, either Google or Microsoft. This ensures that my page will load faster for my visitors, since the jQuery file will get sent to them from their nearest location, gzipped and compressed. When the visitor visits another site that use the same jQuery version from the same CDN, they don’t need to wait for their browser to download the library since it’s already in their temporary Internet files.

One of the drawbacks to this approach is when the CDN goes offline or becomes unavailable. Fortunately this has not happened to me – we should however have a backup plan! While reading through Rick Strahl’s presentations on jQuery I came across a great little snippet that will use a local copy of the jQuery library if the CDN is down or in some way unavailable.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
if (typeof jQuery == 'undefined')
	document.write(unescape("%3Cscript src='/scripts/jquery-1.4.2.min.js' type='text/javascript'%3E%3C/script%3E"));
</script>

Exactly what we need!

Detecting Ajax requests on the server

If you use jQuery or ASP.NET AJAX you can easily detect Ajax requests on the server by simply checking for the HTTP_X_REQUESTED_WITH HTTP Header. Both libraries automatically add this to the HTTP Header when they send a request.

Here’s a little code snippet that returns true for Ajax requests.

public static bool IsAjaxRequest()
{
    return HttpContext.Current.Request.Headers["X-Requested-With"] != null && HttpContext.Current.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}

The cool thing about this is that you can use this in a base page or a HTTP Module, and customize the returned data. You could for instance return JSON or XML.

using System;
using System.Web.UI;
 
namespace MyWebApplication
{
    public class BasePage : Page
    {
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
 
            if (Helper.IsAjaxRequest())
            {
                Response.Headers.Clear();
                Response.ContentType = "application/json";
                Response.Write(json output);
                Response.End();
            }
        }
    }
}

Font resizing and printing with jQuery

You’ve probably seen the three A’s on various websites, especially public sector websites; that you can click on to increase the font size on the web site. I’m personally not a huge fan of those. In my opinion it is much better to explain to the user how they can use the built in browser functionality to do this instead. This will also increase the font size on all websites not just the one you created. You could do this by linking to a page with an explanation and screenshots of how to do this in the most popular browsers.

However sometimes I still need to create those three A’s. Below is a little jQuery plugin that does this.

See the demo

Font resizing plugin

This plugin will create an ordered list with three links to increase the font size. The plugin adds three different classes to the body element, which then can be used to set the base font size.

body.small {
    font-size: 80%;
}
 
body.medium {
    font-size: 120%;
}
 
body.large {
    font-size: 140%;
}

The plugin code.

(function($) {
    $.fn.fontresizing = function(customOptions) {
        var options = $.extend({}, $.fn.fontresizing.defaultOptions, customOptions);
        var bodyClasses = '' + options.smallClass + ' ' + options.mediumClass + ' ' + options.largeClass + '';
        return this.each(function() {
            $(this).append('<ol class="' + options.fontresizingClass + '"><li><a href="" class="' + options.smallClass + '">A</a></li><li><a href="" class="' + options.mediumClass + '">A</a></li><li><a href="" class="' + options.largeClass + '">A</a></li></ol>');
 
            $('ol.' + options.fontresizingClass + ' a').click(function() {
                var cssClass = $(this).attr('class');
                $('body').removeClass(bodyClasses).addClass(cssClass);
                createCookie('fontresizingClass', cssClass, options.cookieDuration);
 
                return false;
            });
 
            var fontresizingClass = readCookie('fontresizingClass');
            if (fontresizingClass == options.smallClass || fontresizingClass == options.mediumClass || fontresizingClass == options.largeClass) {
                $('body').removeClass(bodyClasses).addClass(fontresizingClass);
            }
 
            // cookie functions http://www.quirksmode.org/js/cookies.html
            function createCookie(name, value, days) {
                if (days) {
                    var date = new Date();
                    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                    var expires = "; expires=" + date.toGMTString();
                }
                else var expires = "";
                document.cookie = name + "=" + value + expires + "; path=/";
            }
 
            function readCookie(name) {
                var nameEQ = name + "=";
                var ca = document.cookie.split(';');
                for (var i = 0; i < ca.length; i++) {
                    var c = ca[i];
                    while (c.charAt(0) == ' ') c = c.substring(1, c.length);
                    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
                }
 
                return null;
            }
        });
    };
 
    $.fn.fontresizing.defaultOptions = {
        smallClass: 'small',
        mediumClass: 'medium',
        largeClass: 'large',
        fontresizingClass: 'font-resizing',
        cookieDuration: 365
    };
})(jQuery);

Breaking it down

We start by creating a private scope for our jQuery code. The reason for this is so we don’t have to worry about conflicts with other libraries.

(function($) {
...
})(jQuery);

We then add a new method to the jQuery.fn object

(function($) {
    $.fn.fontresizing = function(customOptions) {
    ...
    };
})(jQuery);

Combine the custom options with the default options.

(function($) {
    $.fn.fontresizing = function(customOptions) {
        var options = $.extend({}, $.fn.fontresizing.defaultOptions, customOptions);
        ...
    };
 
    $.fn.fontresizing.defaultOptions = {
        smallClass: 'small',
        mediumClass: 'medium',
        largeClass: 'large',
        fontresizingClass: 'font-resizing',
        cookieDuration: 365
    };
})(jQuery);

Next we iterate over the jQuery object or jQuery wrapper set, and return “this” (the current jQuery object), so we don’t break the jQuery chaining functionality.

(function($) {
    $.fn.fontresizing = function(customOptions) {
        var options = $.extend({}, $.fn.fontresizing.defaultOptions, customOptions);
        var bodyClasses = '' + options.smallClass + ' ' + options.mediumClass + ' ' + options.largeClass + '';
        return this.each(function() {
        ...
        });
    };
 
    $.fn.fontresizing.defaultOptions = {
        smallClass: 'small',
        mediumClass: 'medium',
        largeClass: 'large',
        fontresizingClass: 'font-resizing',
        cookieDuration: 365
    };
})(jQuery);

Now we come to the actual code for creating the links and attaching click events to them.

(function($) {
    $.fn.fontresizing = function(customOptions) {
        var options = $.extend({}, $.fn.fontresizing.defaultOptions, customOptions);
        var bodyClasses = '' + options.smallClass + ' ' + options.mediumClass + ' ' + options.largeClass + '';
        return this.each(function() {
            $(this).append('<ol class="' + options.fontresizingClass + '"><li><a href="" class="' + options.smallClass + '">A</a></li><li><a href="" class="' + options.mediumClass + '">A</a></li><li><a href="" class="' + options.largeClass + '">A</a></li></ol>');
 
            $('ol.' + options.fontresizingClass + ' a').click(function() {
                var cssClass = $(this).attr('class');
                $('body').removeClass(bodyClasses).addClass(cssClass);
                createCookie('fontresizingClass', cssClass, options.cookieDuration);
 
                return false;
            });
            ...
        });
    };
 
    $.fn.fontresizing.defaultOptions = {
        smallClass: 'small',
        mediumClass: 'medium',
        largeClass: 'large',
        fontresizingClass: 'font-resizing',
        cookieDuration: 365
    };
})(jQuery);

Lastly we have the cookie functions to read and create the cookie to hold the font size choices the user has made.

...
            var fontresizingClass = readCookie('fontresizingClass');
            if (fontresizingClass == options.smallClass || fontresizingClass == options.mediumClass || fontresizingClass == options.largeClass) {
                $('body').removeClass(bodyClasses).addClass(fontresizingClass);
            }
 
            // cookie functions from http://www.quirksmode.org/js/cookies.html
            function createCookie(name, value, days) {
                if (days) {
                    var date = new Date();
                    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                    var expires = "; expires=" + date.toGMTString();
                }
                else var expires = "";
                document.cookie = name + "=" + value + expires + "; path=/";
            }
 
            function readCookie(name) {
                var nameEQ = name + "=";
                var ca = document.cookie.split(';');
                for (var i = 0; i < ca.length; i++) {
                    var c = ca[i];
                    while (c.charAt(0) == ' ') c = c.substring(1, c.length);
                    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
                }
 
                return null;
            }
...

Usage

To use the plugin simply add a reference to the jQuery library file, and copy the code into your page. After you’ve done that you can use it by calling the fontresizing method

// With default options
jQuery(function($) {
    $('#tools').fontresizing();
});
 
// With custom options
jQuery(function($) {
    $('#tools').fontresizing({
        smallClass: 'small',
        mediumClass: 'medium',
        largeClass: 'large',
        fontresizingClass: 'font-resizing',
        cookieDuration: 365
    });
});

Print plugin

Another thing that many clients ask for is a print link. Below is a little jQuery plugin that creates the link and attaches a click event that triggers the browsers print dialog.

(function($) {
    $.fn.print = function(customOptions) {
        var options = $.extend({}, $.fn.print.defaultOptions, customOptions);
 
        return this.each(function() {
            $(this).append('<a href="" class="' + options.printClass + '">' + options.printText + '</a>');
 
            $('a.' + options.printClass + '').click(function() {
                window.print();
                return false;
            });
        });
    };
 
    $.fn.print.defaultOptions = {
        printClass: 'print',
        printText: 'Print'
    };
})(jQuery);

Here we also generate the link with JavaScript, so that users who don’t have JavaScript wont see it – since they cannot use it anyway. When you click on the link you’ll see the print dialog; use this with CSS to create a printer friendly version of your page.

To create CSS rules for print either use the @media

@media print {
  /* CSS rules here */
}

or link to an external CSS file and add the media=”print” attribute.

<link rel="stylesheet" href="styles/print.css" media="print" type="text/css" />

Usage

// With default options
jQuery(function($) {
    $('#tools').print();
});
 
// With custom options
jQuery(function($) {
    $('#tools').print({
        printText: 'Print this page'
    });
});

Download the code or see the demo

Extending search field with suggestion box

Disclaimer: In this example I’ve kept the code simple to make it easier to read and to give you an idea of how you might approach something like this with EPiServer. This code should not be used in production scenarios since it will use a lot of resources. Use at own risk :) .

Update – 12.10.2009

Updated production code with a new custom filter that removes pages that the Everyone role doesn’t have access to.
Also updated the Get caching method. See comments below for further details.

Update – 19.09.2009

Added example of production code you might consider.

We’ve all used Google Suggest before. When you start typing a word or sentence, Google comes up with suggestions to what we might be searching for.
Example of Google Suggest
This is to help users quickly find what they want.

In this blog post we’re going to do the same by extending QuickSearch that ships with the EPiServer Public Templates.

I decided to use jQuery and the AutoComplete plugin for this, but you can use any script you like (most are pretty similar).

The result

Example of search suggest

The Code

First start by modifying the QuickSearch.ascx:

<asp:Panel runat="server" CssClass="QuickSearchArea" DefaultButton="SearchButton">
	<asp:Label ID="SearchLabel" runat="server" AssociatedControlID="SearchText" CssClass="hidden" Text="<%$ Resources: EPiServer, navigation.search %>" />
    <asp:TextBox ID="SearchText" TabIndex="1" runat="server" CssClass="quickSearchField" autocomplete="off" />
    <asp:ImageButton ID="SearchButton" runat="server" ImageUrl="~/Templates/Public/Images/MainMenuSearchButton.png" ToolTip="<%$ Resources: EPiServer, navigation.search %>" CausesValidation="false" CssClass="quickSearchButton" OnClick="Search_Click" />
</asp:Panel>
 
<script type="text/javascript">
//<![CDATA[ 
    $("#<%=SearchText.ClientID %>").autocomplete("/path/to/file/SearchSuggest.aspx", {
        minChars: 2,
        max: 10
    });
//]]> 
</script>

A few things to note. I removed the default text from the TextBox and added the attribute autocomplete=”off”, this is to ensure that the users browser does not store the information (How to Turn Off Form Autocompletion). I then attached the AutoComplete plugin to the TextBox and changed a few of its default properties.

As you can see there is not much code that is needed when mostly using the default settings. You can of course do a lot more. Browse through the AutoComplete plugin documentation and see the demos to get a few ideas.

Now for the main part, SearchSuggestion.aspx. This is just an empty web forms page with all the code in the code-behind:

using System;
using System.Text;
using System.Web;
using EPiServer;
using EPiServer.Core;
using EPiServer.Filters;
using EPiServer.Security;
 
namespace FV.Templates.FV.Pages
{
    public partial class SearchSuggest : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            // #1
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
 
                // #2
                var criterias = new PropertyCriteriaCollection();
 
                var queryCriteria = new PropertyCriteria
                                        {
                                            Condition = CompareCondition.StartsWith,
                                            Name = "PageName",
                                            Value = query,
                                            Type = PropertyDataType.String,
                                            Required = true
                                        };
 
                var pubCriteria = new PropertyCriteria
                                      {
                                          Condition = CompareCondition.Equal,
                                          Name = "PagePendingPublish",
                                          Value = false.ToString(),
                                          Type = PropertyDataType.Boolean,
                                          Required = true
                                      };
 
                criterias.Add(queryCriteria);
                criterias.Add(pubCriteria);
 
                // #3
                var pages = DataFactory.Instance.FindPagesWithCriteria(PageReference.StartPage, criterias, CurrentPage.LanguageBranch);
 
                int tempLimit = Convert.ToInt32(limit);
                int pagesCount = pages.Count;
 
                // #4
                if (tempLimit > pagesCount)
                {
                    tempLimit = pagesCount;
                }
 
                // #5
                for (int i = 0; i < tempLimit; i++)
                {
                    sb.Append(pages[i].PageName);
                    sb.Append(Environment.NewLine);
                }
 
                // #6
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
    }
}
  • In #1 we retrieve the user input and the limit (which we set earlier with the max property in QuickSearch.ascx), we then HTML encode it (something you should always do when receiving user input).
  • In #2 we build the criteria collection with our search options. We search for pages that start with what the user typed in and then make sure that only published pages are returned.
  • In #3 we do the actual searching with the FindPagesWithCriteria method where we also set the current language branch (only return pages in that language).
  • In #4 we make sure that we don’t have less pages than the limit, if so we set the pages count to be the limit.
  • In #5 we go through pages (PageDataCollection) and add the PageName to sb (StringBuilder).
  • In #6 we return the result back

Below is the code for SearchSuggest.aspx.cs updated to use the SearchDataSource instead of FindPagesWithCriteria (this will not only search the PageName but all properties that are search able):

using System;
using System.Text;
using System.Web;
using System.Web.UI;
using EPiServer;
using EPiServer.Core;
using EPiServer.Security;
using EPiServer.Web.WebControls;
 
namespace FV.Templates.FV.Pages
{
    public partial class SearchSuggest : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
 
                var searchDataSource = new SearchDataSource
                {
                    SearchQuery = query,
                    AccessLevel = AccessLevel.Read,
                    PublishedStatus = PagePublishedStatus.Published,
                    PageLink = PageReference.StartPage,
                    LanguageBranches = CurrentPage.LanguageBranch,
                    MaxCount = Convert.ToInt32(limit)
                };
 
                foreach (PageData page in searchDataSource.Select(DataSourceSelectArguments.Empty))
                {
                    sb.Append(page.PageName);
                    sb.Append(Environment.NewLine);
                }
 
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
    }
}

Production code

Like I said in the beginning, this code should not be used in production scenarios since it goes through the entire site searching for matching PageNames each time there is a request.

I’ve added some example code of how you might approach this in a production scenario. Basically I just use the FindPagesWithCriteria method to find all the pages and then store the PageName of each in an array that I then store in the cache for easy access.

using System;
using System.Linq;
using System.Text;
using System.Web;
using EPiServer;
using EPiServer.Core;
using EPiServer.Filters;
using EPiServer.Security;
using FV.Templates.FV.Filters;
 
namespace FV.Templates.FV.Pages
{
    public partial class ProductionSearchSuggestion : TemplatePage
    {
        protected override void OnLoad(EventArgs e)
        {
            string query = HttpUtility.HtmlEncode(Request.QueryString["q"]);
            string limit = HttpUtility.HtmlEncode(Request.QueryString["limit"]);
 
            if (!string.IsNullOrEmpty(query))
            {
                var sb = new StringBuilder();
                string[] pageNames;
 
                const string pageNamesCacheKey = "AllPageNames";
 
                if (!Get(pageNamesCacheKey, out pageNames))
                {
                    var criterias = new PropertyCriteriaCollection();
 
                    var pubCriteria = new PropertyCriteria
                    {
                        Condition = CompareCondition.Equal,
                        Name = "PagePendingPublish",
                        Value = false.ToString(),
                        Type = PropertyDataType.Boolean,
                        Required = true
                    };
 
                    criterias.Add(pubCriteria);
 
                    var pages = DataFactory.Instance.FindPagesWithCriteria(PageReference.StartPage, criterias, CurrentPage.LanguageBranch);
 
                    new FilterRole("Everyone").Filter(pages);
 
                    var pagesCount = pages.Count;
                    pageNames = new string[pagesCount];
 
                    for (int i = 0; i < pagesCount; i++)
                    {
                        pageNames[i] = pages[i].PageName;
                    }
 
                    Add(pageNames, pageNamesCacheKey);
                }
 
                int tempLimit = Convert.ToInt32(limit);
 
                var result = new string[tempLimit];
                int resultCounter = 0;
 
                for (int i = 0; i < pageNames.Length; i++)
                {
                    if (resultCounter <= tempLimit && pageNames[i].ToLower().StartsWith(query.ToLower()) && !result.Contains(pageNames[i]))
                    {
                        result[resultCounter++] = pageNames[i];
                    }
                }
 
                for (int i = 0; i < resultCounter; i++)
                {
                    sb.Append(result[i]);
                    sb.Append(Environment.NewLine);
                }
 
                Response.AddHeader("Content-Type", "text/html");
                Response.Write(sb.ToString());
 
                Response.End();
            }
        }
 
        // Caching methods from http://johnnycoder.com/blog/2008/12/10/c-cache-helper-class/
        public bool Exists(string key)
        {
            return HttpContext.Current.Cache[key] != null;
        }
 
        public bool Get<T>(string key, out T value) where T:class
        {
            try
            {
                value = HttpContext.Current.Cache[key] as T;
                if (value == null)
                {
                    value = default(T);
                    return false;
                }
            }
            catch
            {
                value = default(T);
                return false;
            }
 
            return true;
        }
 
        public void Add<T>(T o, string key)
        {
            HttpContext.Current.Cache.Insert(
                key,
                o,
                null,
                DateTime.Now.AddMinutes(3600),
                System.Web.Caching.Cache.NoSlidingExpiration);
        }
    }
}

The FilterRole class

using System;
using EPiServer.Core;
using EPiServer.Filters;
 
namespace FV.Templates.FV.Filters
{
    public class FilterRole : IPageFilter
    {
        public string RoleName
        {
            get;
            set;
        }
 
        public FilterRole()
        {
        }
 
        public FilterRole(string roleName)
        {
            this.RoleName = roleName;
        }
 
        public void Filter(PageDataCollection pages)
        {
            for (int i = pages.Count - 1; i >= 0; i--)
            {
                bool isMatch = false;
                var acl = pages[i].ACL;
 
                if (acl != null)
                {
                    foreach (var role in acl)
                    {
                        if (role.Key == this.RoleName)
                        {
                            isMatch = true;
                        }
                    }
                }
 
                if (!isMatch)
                {
                    pages.RemoveAt(i);
                }
            }
        }
 
        public void Filter(object sender, FilterEventArgs e)
        {
            this.Filter(e.Pages);
        }
 
        public bool ShouldFilter(PageData page)
        {
            throw new NotImplementedException();
        }
    }
}

Download the code

ASP.NET web forms and jQuery Thickbox plugin

Update

Thickbox is no longer supported. Use another of jQuery’s overlay/lightbox plugins. This post will remain for legacy purposes only.

I’m very found of jQuery, and use the Thickbox plugin a lot in my project. I’ve not used it much for images, but rather for content. To retrieve content you have three options.

  • Inline content
  • Iframed content
  • Ajax content

Inline content

This is pretty straight forward, you have some content on the same page that you’d like to display inside the Thickbox. This one I use a lot, since it is easy to make accessible for search engines, screen readers and other devices. You simply hide the content with JavaScript (in jQuery you can use the hide function: $(selector).hide();), and then trigger opening the content inside the Thickbox with either a link or button.

<a href="#TB_inline?height=155&amp;width=300&amp;inlineId=contentId" class="thickbox">Show hidden content.</a>

Iframe content

<a href="Default.aspx?keepThis=true&amp;TB_iframe=true&amp;height=300&amp;width=500" title="thickbox title" class="thickbox">Iframe content</a>

The nice thing about this is that you can easily include existing pages inside the Thickbox. Another thing to note is that users without JavaScript, like search engines, still can navigate the content, since the a element’s href attribute points to the page. This is also the easiest way to still have functionality that you have inside your ASP.NET web form page like Postbacks, sessions etc.

Ajax content

Like with iframed content people without JavaScript will still be able get to the content.

<a href="ajaxContent.aspx?height=300&amp;width=300" title="thickbox title" class="thickbox">Ajax conten</a>

The one problem I’ve had in the past with this method is when posting data back to the server with ASP.NET web forms (same problem with inline content). Until recently I always used iframed content when having to deal with form controls. The reason being that the content inside the Thickbox got added outside the form element on the page (right before the closing body tag).

Thickbox code right before closing body tag

To fix this you need to change this line of code inside the thickbox.js file.

$("body").append("<div id='TB_overlay'></div><div id='TB_window'></div>");

Add form:first to the selector to target the first form element on the page.

$("body form:first").append("<div id='TB_overlay'></div><div id='TB_window'></div>");

Thickbox code inside the form tag now

Now the form controls will work and you’ll be able to have the user fill out the form inside the Thickbox and post it back to the server.

The Thickbox is also very easy to style and extend, below are some screen shots of sites where I’ve used it.

thickbox screen shot

Thickbox screen shot

Other resources

© Copyright Frederik Vig. Based on Fluid Blue theme