Search

Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Thursday, 30 June 2011

A D.I.Y. Lightbox


Introduction

OK, so JavaScript/jQuery lightboxes are ten-a-penny. Just go to the jQuery plugin repository and take your pick! Indeed for long time, I did just that. After all, there's no point re-inventing the wheel is there??

However, I recently had to write my own JavaScript lightbox from scratch. So I thought now was a good opportunity to share my experiences with you, as well as the code, which you are free to use should you want or need to develop your own lightbox or maybe want to develop my lightbox further.

LiteBox

The fruits of my labours I have (unimaginatively) named 'LiteBox', mainly to try and emphasise the fact that it makes no pretences to doing anything fancy! Also, as I am a shameless jQuery whore, I am also assuming prior experience of jQuery. If you are unfamiliar with jQuery, you can find more information here.

The Basics

LiteBox is implemented as a jQuery plugin. The signature is as follows:

jQuery.lightbox( url, [options] )

The url parameter is mandatory and specifies the URL of the image to be shown in the lightbox. The options parameter is a set of optional key/value pairs for configuring the lightbox:

KeyDescriptionDefault Value
titleThe text to be used for the tooltip and alt attribute of the image.The URL of the image.
showCloseButtonWhether or not to show the 'close' button on the lightbox.True.
closeButtonPositionThe position of the 'close' button.
Can be one of either 'top-left', 'top-right', 'bottom-left' or 'bottom-right'.
Ignored if showCloseButton is false.
'top-right'.
animationSpeedThe speed of the animation.
Can be one of either 'slow', 'medium' or 'fast'; or the length of the animation in milliseconds.
'medium' (≡ 500ms)

Getting into the Code

Before we get delve too deeply into the JavaScript, it's helpful to have a look at the CSS:

.jquery-litebox-lightbox 
{
 position: absolute;
 background-color: Transparent;
 z-index: 1001;
 margin: 0px;
 padding: 0px;
 border: 10px solid white;
}

.jquery-litebox-img
{
    margin: 0px;
    padding: 0px;
}

.jquery-litebox-close
{
    height: 25px;
    width: 25px;
    position: absolute;
    cursor: pointer;
    background-image: url(images/jquery-litebox_close.png);
    background-repeat: no-repeat;
    z-index: 1002;
}

.jquery-litebox-shadow 
{
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 background-image: url(images/jquery-litebox_shadow.png);
 background-repeat: repeat;
}

As is customary with jQuery plugins, the first thing we need to do is merge any user options into a settings object:

var settings = {
    title: url,
    showCloseButton: true,
    closeButtonPosition: 'top-right',
    animationSpeed: 'medium'
};
$.extend(settings, options);

The next thing to do is create a <img> tag for the full-sized image, and add it to the DOM. It is important that we do this early on as we need the browser to have loaded the image in order to work with its properties, such as height and width etc. However, we make the image invisible so that it doesn't actually appear to the user yet:

var img = $('<img src="' + url + '" alt="' + settings.title + '" title="' + settings.title + '" class="jquery-litebox-img" style="display: none;" />');
$('body').append(img);

Once the browser has loaded them image, then the interesting stuff can start. By hooking into the image's load event we can create the lightbox and append the image to it:

var imgWidth = $(this).width();
var imgHeight = $(this).height();
$(this).detach().height('0px').width('0px');
var lightbox = $('<div class="jquery-litebox-lightbox"></div>').css('top', $(window).scrollTop() + 100 + 'px').css('left', ($(window).width() / 2) - (imgWidth / 2) + 'px');
var shadow = $('<div class="jquery-litebox-shadow"></div>');
$('body').append(shadow);
$('body').append(lightbox);
lightbox.append($(this));

As you can see, we firstly get the height and width of the image. We then detach the image from the DOM and set its height and width to 0px.

Next, we create the lightbox itself. Now that we know the height and width of the image, we can calculate and set the position of the top-left-hand corner of the lightbox such that it will be centred horizontally on the page and 100px from the top of the window.

We then create the lightbox shadow and append it to the DOM. We append the lightbox itself to the DOM and then append the image to the lightbox.

Now the time has come to animate our lightbox. The animation is very simple: we simply grow the image from top-left to bottom-right until the image is at its full size. This is achieved using jQuery's animate() function:

var animationTime = getAnimationTime(settings.animationSpeed);
$(this).show().animate({
    width: imgWidth,
    height: imgHeight
}, animationTime, function () {
    if (settings.showCloseButton) {
        showCloseButton(settings.closeButtonPosition);
        img.mouseover(function () {
            showCloseButton(settings.closeButtonPosition);
        });
        img.mouseout(function (e) {
            hideCloseButton(e);
        });
    }
});

When the animation is complete, we hook into the mouseover and mouseout events of the image to show and hide the close button respectively. The code for showing and hiding the button is as follows:

function showCloseButton(position) {
    var img = $("img.jquery-litebox-img");
    var imgPositionY = img.offset().top;
    var imgPositionX = img.offset().left;
    var imgHeight = img.height();
    var imgWidth = img.width();
    if ($("div.jquery-litebox-close").length == 0) {
        var close = $('<div class="jquery-litebox-close" title="Close the lightbox." style="display: none;"></div>');
        $('body').append(close);
        switch (position) {
            case 'top-left':
                close.css('top', imgPositionY).css('left', imgPositionX);
                break;
            case 'top-right':
                close.css('top', imgPositionY).css('left', (imgPositionX + imgWidth) - close.width());
                break;
            case 'bottom-left':
                close.css('top', (imgPositionY + imgHeight) - close.height()).css('left', imgPositionX);
                break;
            case 'bottom-right':
                close.css('top', (imgPositionY + imgHeight) - close.height()).css('left', (imgPositionX + imgWidth) - close.width());
                break;
            default:
                throw new Error("Buttom position must be one of either: 'top-left', 'top-right', 'bottom-left' or 'bottom-right'.");
        }
        close.click(function (e) {
            $(this).remove();
            closeLightBox();
        });
        close.show();
    }
}

function hideCloseButton(mouseEvent) {
    if (!isIn($("div.jquery-litebox-close"), mouseEvent))
        $("div.jquery-litebox-close").remove();
}

function isIn(obj, mouseEvent) {
    if (obj.length > 0) {
        var x = mouseEvent.pageX;
        var y = mouseEvent.pageY;
        var posX = obj.position().left;
        var posY = obj.position().top;
        var objX = obj.width();
        var objY = obj.height();
        return x > posX && x < posX + objX && y > posY && y < posY + objY;
    }
    else
        return false;
}

The animation time is determined by calling the getAnimationTime() function:

function getAnimationTime(speed) {
    if (typeof speed === 'string') {
        switch (speed) {
            case 'slow': return 1000;
            case 'medium': return 500;
            case 'fast': return 250;
            default:
                var parsedSpeed = parseInt(speed);
                if (!isNaN(parsedSpeed))
                    return parsedSpeed;
                else
                    throw new Error("Animation speed must be a number or one of: 'slow', 'medium' or 'fast'.");
        }
    }
    else if (typeof speed === 'number')
        return speed;
    else
        throw new Error("Animation speed must be a number or one of: 'slow', 'medium' or 'fast'.");
}

Summary

LiteBox is a very simple, lightweight jQuery lightbox, which can serve as an example for anyone wishing to develop their own solution; or as a base for anyone wishing to extend it further.

You can download the source code, along with a sample web page from here.

So can see a demo of LiteBox in action here.

Thursday, 28 April 2011

Accessing MVC Routing URLs in JavaScript


As we all know, the MVC framework for ASP.NET uses the .NET routing engine, introduced in .NET 3.5, for generating and resolving URLs at runtime. As such, we can use the UrlHelper class to query the routing engine and generate the correct URL for us. For example:

<a href='<%= Url.Action("MyAction", "MyController") %>'>Foo<a>

will generate the following at runtime:

<a href='/MyController/MyAction'>Foo<a>

This is great, as it means any changes we make to our routing rules are automagically reflected any affected URLs. Additionally, the routing engine will take into account where on our web-server our application is located. In the example above, if our application were deployed to virtual directory called MySite, the routing engine would generate the following:

<a href='/MySite/MyController/MyAction'>Foo<a>

Well that's fine and dandy, but what happens when we want to use URLs in JavaScript? Consider the following simple scenario: A simple web-page with a text-box and a button. You enter a person ID into the text-box, click the button; and via an AJAX request, the person's details are displayed on the page. We have a controller:

// C#
public class PersonController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Details(int id)
    {
        PersonService service = new PersonService();
        Person person = service.GetPerson(id);
        return PartialView(person);
    }
}
' Visual Basic
Public Class PersonController
    Inherits System.Web.Mvc.Controller

    Public Function Index() As ActionResult
        Return View()
    End Function

    Public Function Details(ByVal id As Integer) As ActionResult
        Dim service As PersonService = New PersonService()
        Dim person As Person = service.GetPerson(id)
        Return PartialView(person)
    End Function

End Class

A view:


<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title>Person</title>
    <script type="text/javascript" src='<%= Url.Content("~/Scripts/jquery-1.5.1.min.js") %>'></script>
</head>
<body>
    <div>
        <input id="personId" type="text" />
        <input id="getPersonButton" type="button" value="Get Person" />
    </div>
    <div id="personPlaceHolder">
    </div>
    <script type="text/javascript">
        $(document).ready(function () {
            $("#getPersonButton").click(function () {
                var id = $("#personId").val();
                getPerson(id);
            });
        });

        function getPerson(id) {
            var url = '/Person/Details/' + id;
            $.get(url, function (result) {
                $("#personPlaceHolder").html(result);
            });
        }

    </script>
</body>
</html>

<%@ Page Language="VB" Inherits="System.Web.Mvc.ViewPage" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title>Person</title>
    <script type="text/javascript" src='<%= Url.Content("~/Scripts/jquery-1.5.1.min.js") %>'></script>
</head>
<body>
    <div>
        <input id="personId" type="text" />
        <input id="getPersonButton" type="button" value="Get Person" />
    </div>
    <div id="personPlaceHolder">
    </div>
    <script type="text/javascript">
        $(document).ready(function () {
            $("#getPersonButton").click(function () {
                var id = $("#personId").val();
                getPerson(id);
            });
        });

        function getPerson(id) {
            var url = '/Person/Details/' + id;
            $.get(url, function (result) {
                $("#personPlaceHolder").html(result);
            });
        }

    </script>
</body>
</html>

And a partial view:


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Person>" %>
<fieldset>
    <legend>Person</legend>
    <table>
        <tbody>
            <tr>
                <th>
                    ID
                </th>
                <td>
                    <%: Html.DisplayFor(model => model.Id) %>
                </td>
            </tr>
            <tr>
                <th>
                    Name
                </th>
                <td>
                    <%: Html.DisplayFor(model => model.Name) %>
                </td>
            </tr>
            <tr>
                <th>
                    Date of Birth
                </th>
                <td>
                    <%: Html.DisplayFor(model => model.DateOfBirth) %>
                </td>
            </tr>
        </tbody>
    </table>
</fieldset>

<%@ Control Language="VB" Inherits="System.Web.Mvc.ViewUserControl(Of Person)" %>
<fieldset>
    <legend>Person</legend>
    <table>
        <tbody>
            <tr>
                <th>
                    ID
                </th>
                <td>
                    <%: Html.DisplayFor(Function(model) model.Id)%>
                </td>
            </tr>
            <tr>
                <th>
                    Name
                </th>
                <td>
                    <%: Html.DisplayFor(Function(model) model.Name)%>
                </td>
            </tr>
            <tr>
                <th>
                    Date of Birth
                </th>
                <td>
                    <%: Html.DisplayFor(Function(model) model.DateOfBirth)%>
                </td>
            </tr>
        </tbody>
    </table>
</fieldset>

Now look closely at the getPerson() function:

function getPerson(id) {
    var url = '/Person/Details/' + id;
    $.get(url, function (result) {
        $("#personPlaceHolder").html(result);
    });
}

You will see that the URL is hard-coded and the person ID is simply concatenated onto the end. Clearly this will break if we either change our routing rules, or deploy the application to anywhere other the root of our web-server. One solution is to place an ASP.NET call to the routing engine within the string literal used for the URL:

// C#
function getPerson(id) {
    var url = '<%= Url.Action("Details", "Person", new { Id = -999 }) %>'.replace('-999', id);
    $.get(url, function (result) {
        $("#personPlaceHolder").html(result);
    });
}
// Visual Basic
function getPerson(id) {
    var url = '<%= Url.Action("Details", "Person", New With {.Id = -999}) %>'.replace('-999', id);
    $.get(url, function (result) {
        $("#personPlaceHolder").html(result);
    });
}

Note how we use a 'dummy' value of "-999" for the person ID, to ensure that the routing engine resolves the URL correctly. We then replace this value with the 'real' ID at runtime.

However, what if we want to move the getPerson() function to a separate .js file? Our ASP.NET call to the routing engine will no longer work as the .js file is not processed server-side. The solution is to use a JavaScript helper object:

// C#
function UrlHelper() {
    this.personDetails = function (id) {
        return '<%= Url.Action("Details", "Person", new { Id = -999 }) %>'.replace('-999', id);
    }
}
// Visual Basic
function UrlHelper() {
    this.personDetails = function (id) {
        return '<%= Url.Action("Details", "Person", New With {.Id = -999}) %>'.replace('-999', id);
    }
}

The helper object must still remain in the view, as the call to the routing engine needs to be processed server-side. However, our getPerson() function can now be modified to use the helper object and be safely moved into a separate .js file:

function getPerson(id) {
    var urlHelper = new UrlHelper();
    var url = urlHelper.personDetails(id);
    $.get(url, function (result) {
        $("#personPlaceHolder").html(result);
    });
}