/**
@fileoverview
Unobtrusive enhancements to core site functionality. This is the primary site javascript file.
	
@requires <a href="http://jqueryjs.googlecode.com/files/jquery-1.2.6.min.js">jQuery 1.2.6</a>
@requires <a href="http://www.stanlemon.net/projects/jgrowl.html">jGrowl 1.1.1</a>
	
@author Jon.Burger, <a href="http://think.eu">TH_NK</a>
@version 1.0
*/

var root;

/** Calls the 'init' functions when the document has loaded */
$(document).ready(function() {
    root = $('script[@src*="_assets/"]:eq(0)').attr('src').split('_assets')[0];

    SearchBox();
    Blockquote();
    Inbox();
    QuickLogin();
    TagExpander('.strengths');
    TagExpander('.weaknesses');
    TagExpander('.user_profile .business');
    ProfileContentExpander();
    POBAnswersExpander();
    POBSummariesExpander();
    POBQuestionsExpander();
    FilterExpander();
    SearchMain();
    //EntrepreneurExpander();
    CallsForHelpExpander();
    ConversationExpander();
    MessageHistoryExpander();
    AddTagsPanel();
    //StartGrowling();
    ZebraStripe();
    ExternalLinks();
    UserPanel();
    FocusMessageBody();
    BigLinks();
    $("#acc").accordion();
    $("#ctl00_ctl00_cphAllContent_cphMainColumn_ddlHeardOfChallenge").other();
    $('textarea.help').hideFieldText();

    FaqExpander();
});

/** Accordian **/
jQuery.fn.other = function() {
    $(this).parent(".frmRow").next().addClass("hidden");

    $(this).blur(checkOther);

    function checkOther() {
        if ($(this).find("option:selected").val() == 7) {
            $(this).parent(".frmRow").next().removeClass("hidden");
        }
        else {
            $(this).parent(".frmRow").next().addClass("hidden");
        }
    }
    checkOther();
}

/** Accordian **/
jQuery.fn.accordion = function() {
    if ($(this).length > 0) {
        $(this).find("dt").wrapInner("<a href='#'></a>");
        $(this).find("dt").addClass("closed");
        $(this).find("dd").addClass("hidden");
        $(this).find("dt a").click(function(e) {
            e.preventDefault();
            if ($(this).parent("dt").next("dd").hasClass("hidden") == true) {
                $(this).parent("dt").removeClass("closed")
                .siblings("dt").addClass("closed").end()
                .siblings("dd:visible").addClass("hidden").end()
                .next("dd").removeClass("hidden");
            }
        });
    }
}

/** Make links have a larger click area ***/
function BigLinks() {
    $('.big_picture').bigLink();
    $('.panel.talk h2').bigLink('.panel.talk a:eq(0)');
}

/** Make all links with class of 'external' open in a new window when clicked  **/
function ExternalLinks() {
    var ico_path = root + '_assets/css/icons/new_window.png';

    $('a.external')
	.click(function(e) {
	    return !window.open(this.href);
	})
	.append('&nbsp;<img class="ico_new_window" src="' + ico_path + '" alt="opens in a new window" />')
	.each(function(i) {
	    var title = $(this).attr('title');

	    if (title && !/^\s*$/.test(title)) {
	        $(this).attr('title', title + ' : opens in a new window');
	    }
	    else {
	        $(this).attr('title', 'opens in a new window');
	    }
	});
}

/** Zebra stripe tables by adding a class of 'odd' on odd rows **/
function ZebraStripe() {
    $('tr:odd').addClass('odd');
}

/**
Initiates jGrowl functionality for system notifications
	
@requires <a href="http://www.stanlemon.net/projects/jgrowl.html">jGrowl 1.1.1</a>
*/
function StartGrowling() {
    // maximum number of failed concurrent attempts to connect to the notifications web service
    var max_attempts = 5;

    // keeps track of concurrent failed attempts - gets reset upon a successful connect
    var attempt = 1;

    // time in unix time to stop polling - after 10 minutes on a single page it is assumed the user has left
    var timeout = new Date().getTime() + (60000 * 10);

    /** Accepts an array of notification objects and displays them as 'growls' */
    function doGrowl(/**Object[]*/notifications) {
        for (var i = 0, l = notifications.length; i < l; i++) {
            var params = notifications[i];

            if (params.message) {
                // call to jGrowl plugin with message and server-sent params
                $.jGrowl(params.message, params);
            }
        }
    }

    /** Server polling function, handles making timely requests to the server and passing valid responses to {@link doGrowl} */
    function pollForGrowls() {
        // data object to pass to the web service
        var data = {};

        // member id required to assist retrieval of notifications (it is also required this member is the current signed in member)
        data.memberID = $('.userpanel [name="memberID"]').val();

        /** Callback for a successful response from the notifications web service */
        function success(/**Object*/rsp) {
            // if object does not contain a notifications array then it is invalid - fail
            if (!rsp.notifications) {
                return fail();
            }

            // if the notifications array contains messages the pass the to {@link doGrowl}
            if (rsp.notifications.length > 0) {
                doGrowl(rsp.notifications);
            }
            /* // for testing
            else
            {
            doGrowl([{header:'System Message', message:'Now new notifications'}]);
            }
            */

            // if the timeout has not been reached, poll the server again in 30 seconds
            if ((new Date()).getTime() < timeout) {
                setTimeout(pollForGrowls, 30000);
            }
            /* // for testing
            else
            {
            doGrowl([{header:'System Timeout', message:'10 minutes per page'}]);
            }*/

            // reset count of unsuccessful concurrent attempts to connect
            attempt = 1;
        }

        /** Callback for a unsuccessful response from the notifications web service */
        function fail() {

            // for testing
            //doGrowl([{header:'System Error', message:'Invalid response from server.'}]);


            // if the timeout has not been reached AND the count of unsuccessful concurrent attempts to connect as not been reached, attempt to reconnect again in 5 seconds
            if ((new Date()).getTime() < timeout && attempt < max_attempts) {
                setTimeout(pollForGrowls, 5000); // should be 5 secs
            }
            /* // for testing
            else
            {
            doGrowl([{header:'System Timeout', message:'10 minutes per page'}]);
            }*/

            // increment unsuccessful concurrent attempts to connect
            attempt++;
        }

        // only start polling the server if a member id has been found
        if (data.memberID) {
            jsonQuery('/growler.ashx', data, success, fail);
        }
    }

    // initiate the polling
    pollForGrowls();
}

/** Focuses the body textarea */
function FocusMessageBody() {
    //BGN focus message body on new email / reply 
    $('#body').focus();
    //END focus message body on new email / reply 
}

/** Initiates the Add Tags input panel */
function AddTagsPanel() {
    // BGN Add Tags auto show/hide label
    $('#txt_add_tags').labelToValue();
    // END Add Tags auto show/hide label 

    // BGN Add Tags 'Go' button hover (IE6)
    if ($.browser.msie && $.browser.version < 7) {
        $('#btn_add_tags').hover(function() {
            this.style.backgroundPosition = '100% -28px';
        },
		function() {
		    this.style.backgroundPosition = '100% 0';
		})
    }
    // END Add Tags 'Go' button hover (IE6)
}

/** Initiates the Answer filter input panel */
function AnswerFilterBox() {
    // BGN Search box auto show/hide label
    $('#txt_answer_search').labelToValue();
    // END Search box auto show/hide label 

    // BGN Search box 'Go' button hover (IE6)
    if ($.browser.msie && $.browser.version < 7) {
        $('#btn_answer_search').hover(function() {
            this.style.backgroundPosition = '100% -28px';
        },
		function() {
		    this.style.backgroundPosition = '100% 0';
		})
    }
    // END Search box 'Go' button hover (IE6)
}
/** Initiates the sliding 'expander' for FAQs */
function FaqExpander() {
    var label = 'View answer';
    var answer = $('.faq-panel .answer');
    var question = $('.faq-panel .question');

    answer.hide()

    $('<div class="content_expander"><a href="#"><span>' + label + '</span></a></div>')
	.insertAfter(answer);

    $(".content_expander").click(function(e) {
        var current = $(this).prev();

        var expandees = current.wrapAll($('<div class="expandees"></div>'))
	    .append('<div class="clear">&nbsp;</div>')
	    .parent()
	    .hide();

        if (current.css("display") == 'none') {
            current.show();
            $(this).find('span').html("Hide answer");
        }
        else {
            current.hide();
            $(this).find('span').html("View answer");
        }

        var speed = Math.min(Math.round(expandees.height() * 3), 3000);

        e.preventDefault();
        expandees // relative for IE, and float for Safari
	        .slideDown(speed, function() {
	            expandees.replaceWith(expandees.html());
	        });

    })
}

/** Initiates the sliding 'expander' for conversations */
function ConversationExpander() {
    var responses = $('.response').length + ' ' || '';
    var label = 'View all ' + responses + 'responses';

    $('.response').contentExpander($('.response:last'), label);
}

/** Initiates the sliding 'expander' for mails */
function MessageHistoryExpander() {
    var history = $('.history').length + ' ' || '';
    var label = 'View message history';

    $('.history').contentExpander($('.history:last'), label);
}

/** Initiates the sliding 'expander' for calls for help */
function CallsForHelpExpander() {
    var calls = $('.calls_for_help .call_for_help').length + ' ' || '';
    var label = 'View all ' + calls + 'calls for help';

    $('.calls_for_help .call_for_help:gt(1)').contentExpander($('.calls_for_help .call_for_help:last'), label);
}

/** Initiates the sliding 'expander' for entrepeuners filter */
function FilterExpander() {
    var strengths = $('.filter_panel .alpha_list:last li').length + ' ' || '';
    var last_h4 = $(".filter_panel h4:last").html();

    if (last_h4 != null) {
        var label = 'View all ' + strengths + last_h4.replace(":", ""); //'strengths';

        $('.filter_panel .alpha_list:last li:gt(30)').contentExpander($('.filter_panel .alpha_list:last'), label, 500);
    }
}

/** Initiates the sliding 'expander' for lists of entrepreneurs */
function EntrepreneurExpander() {
    var entres = $('.entrepreneurs_list .profile').length + ' ' || '';

    var label = 'View all ' + entres + 'entrepreneurs';

    $('.entrepreneurs_list .profile:gt(1), .entrepreneurs_list .profile:gt(1) + .clear').contentExpander($('.entrepreneurs_list .profile:last'), label);
}

/** Initiates the sliding 'expander' for lists of content on a member's profile */
function ProfileContentExpander() {
    var username = '';
    if (username = $('.user_profile .details dt').text().replace(/^(dr|prof|sir|mr|mrs|miss|ms)\.?\s+/i, '').split(' ')[0]) {
        username += (username[username.length - 1] == 's') ? "' " : "'s ";
    }

    var label = 'View ' + username + 'content';

    $('.user_content:gt(1)').contentExpander($('.user_content:last'), label);
}

/** Initiates the sliding 'expander' for lists of answers within Pick Our Brains */
function POBAnswersExpander() {
    var answers = $('.qa_panel .answer').length + ' ' || '';

    var label = 'View all ' + answers + 'answers to this question';

    $('.qa_panel .answer:gt(2)').contentExpander($('.qa_panel dl:not(.commenter)'), label);
}

/** Initiates the sliding 'expander' for lists of question summaries within Pick Our Brains */
function POBSummariesExpander() {
    var questions = $('.questions_panel li').length + ' ' || '';

    var label = 'View all ' + questions + 'summaries';

    $('.questions_panel li:gt(2)').contentExpander($('.questions_panel ul'), label);
}

/** Initiates the sliding 'expander' for lists of questions within Pick Our Brains */
function POBQuestionsExpander() {
    var questions = $('.questions_list li').length + ' ' || '';

    var label = 'View all ' + questions + 'questions';

    $('.questions_list li:gt(2)').contentExpander($('.questions_list ul'), label);
}

/** Initiates show/hide collapsing for lists on a member profile */
function TagExpander(classname) {
    if ($(classname + ' ul li')
	.not(':last')
	.append(',')
	.end()
	.not(':eq(0)')
	.hide()
	.end()
	.length > 1) {
        $(classname).append(
			$('<a href="#" class="expander ico_expand">expand list</a>')
			.click(function(e) {
			    e.preventDefault();

			    var _this = $(this);

			    if (_this.hasClass('ico_expand')) {
			        _this
					.removeClass('ico_expand')
					.addClass('ico_collapse')
					.parent()
					.find('li:gt(0)')
					.show();
			    }
			    else {
			        _this
					.removeClass('ico_collapse')
					.addClass('ico_expand')
					.parent()
					.find('li:gt(0)')
					.hide();
			    }
			})
		);
    }
    else {
        $(classname).append($('<span class="expander">&nbsp;</span>').css({ cursor: 'default' }));
    }
}

/** Initiates show/hide of the 'quick login' when present in source */
function QuickLogin() {
    $('#userlogin').click(function(e) {
        e.preventDefault();

        var search = $('<li class="last"><a href="#">Back to Search</a></li>');

        search.find('a')
		.click(function(e) {
		    e.preventDefault();

		    $('#userbar')
			.removeClass('userlogin')
			.find('ul li')
			.show();

		    $(this).parent().remove();
		});

        $('#userbar')
		.addClass('userlogin')
		.find('ul li')
		.hide()
		.parent()
		.append(search);
    });

    // BGN auto show/hide label
    $('#txt_quickuser, #pwd_quickpass').labelToValue();
    // END auto show/hide label 


    // BGN 'Go' button hover (IE6)
    if ($.browser.msie && $.browser.version < 7) {
        $('#btn_quicklogin').hover(function() {
            this.style.backgroundPosition = '100% -28px';
        },
		function() {
		    this.style.backgroundPosition = '100% 0';
		})
    }
    // END 'Go' button hover (IE6)
}

/** Initiates the main search input on the search page */
function SearchMain() {
    // BGN Search box auto show/hide label
    $('#txt_search_panel').labelToValue();
    // END Search box auto show/hide label 

    // BGN Search box 'Go' button hover (IE6)
    if ($.browser.msie && $.browser.version < 7) {
        $('#btn_search_panel').hover(function() {
            this.style.backgroundPosition = '100% -28px';
        },
		function() {
		    this.style.backgroundPosition = '100% 0';
		})
    }
    // END Search box 'Go' button hover (IE6)
}

/** Initiates the site search input on all pages */
function SearchBox() {

    // BGN Search box auto show/hide label
    $('#txt_search').labelToValue();
    // END Search box auto show/hide label 

    // BGN Search box 'Go' button hover (IE6)
    if ($.browser.msie && $.browser.version < 7) {
        $('#btn_search').hover(function() {
            this.style.backgroundPosition = '100% -28px';
        },
		function() {
		    this.style.backgroundPosition = '100% 0';
		})
    }
    // END Search box 'Go' button hover (IE6)
}

/** @deprecated Equalises column heights - however this functionality requires a more intelligent polling solution if required */
function EqualiseColumnHeight() {
    // BGN equalise height of primary and secondary columns
    $('#secondary').height(Math.max($('#secondary').height(), $('#primary').height()));
    //END equalise height of primary and secondary columns
}

/** Add open quotes to blockquotes and other quoted text */
function Blockquote() {
    // BGN add quotation mark to blockquote	
    var quoted = $('blockquote, .qa_panel .question p:eq(0), .questions_list .question p')
	.prepend('<span class="open_quote"></span>');


    if ($.browser.ie && $.browser.version < 7) {
        quoted.height(Math.max(quoted.height(), 35));
    }
    else {
        var mh = parseInt(quoted.css('min-height'));

        if (mh == NaN || mh < 35) {
            quoted.css('min-height', '35px');
        }
    }
    // END add quotation mark to blockquote
}

/** initiates the user panel - including the autoexpanding text field and ajax saving of updated statuses */
function UserPanel() {
    // BGN User Status Bubble AutoExpander
    if ($.browser.safari) {
        // Chrome also says 'true' for safari
        $('.userbubble textarea').css({ resize: 'none' });
    }

    // the primary visible textarea - set it's rows to 1 (so that at the very least one row will be visible for entering text) and set it's overflow to hidden (remove scrollbars)
    var bubble = $('.userbubble textarea').attr('rows', '1').css('overflow', 'hidden');

    // clone and hide the primary textarea - this clone will provide the scrolling version of the text (so we can steal the scrollheight for our auto sizing textarea)
    var dummy = bubble.clone().css({ position: 'absolute', top: 0, left: 0, visibility: 'hidden' });

    // a little message that will appear when you start editing your status, and will only disappear when you have successfully saved
    var changed = $('<span>changes not saved</span>').css({ float: 'right', margin: '10px -10px 0 0', color: '#999' });

    // insert the cloned textarea before the visible one
    bubble.parent().prepend(dummy);

    // insert the 'changes not saved' message before the save button and hide it for now
    changed.insertBefore('.userpanel #status_save').hide();

    // attach the main resizing function to the visible textarea's 'keyup' event
    bubble.keyup(function(e) {
        // capture the caret position before updating the textarea value with modified version
        var caret = $(e.target).caret();

        // unescape html encoding and remove extraneous trailing newlines (most important for safair), then crop the string to 160 characters (or less if it is not longer)
        var status = e.target.value = unescape(e.target.value).replace(/(\n|\r)+$/g, '\n').substr(0, 160);

        // updated both the visible autosizing textarea and the cloned fixed sized with scrolling 
        // textarea (this textareas scrollheight is what is used to size the visible text area)
        bubble.val(status);
        dummy.val(status);

        // reset the caret position to where is was before the value was updated 
        $(e.target).caret(caret == -1 ? status.length : caret);

        // set the visible textarea's height to the scrollheight of the cloned one
        $(e.target).height(dummy.get(0).scrollHeight);

        // if the content is different from when the field was focused show the 'content changed' message (which only disappears upon successful save)
        if ($(e.target).data('content') && $(e.target).data('content') != e.target.value) {
            changed.show();
        }
    })
    // attach a function that captures the visible textarea's value on focus (used for detecting changes)
	.focus(function(e) {
	    $(e.target).data('content', e.target.value);

	    bubble.keyup();
	})
    // trigger all events - this order is critical to it working in opera, other browsers are less picky
	.keyup()
	.focus()
	.blur()
    // END User Status Bubble AutoExpander

    // BGN Ajax save functionality
    var feedback = $('<div class="feedback">Saving&hellip;</div>');

    feedback.insertAfter('.userpanel #status_save').hide();

    /** Callback for successful save of status */
    function success(/**Object*/data) {
        changed.hide();

        feedback.html('Saved successfully!');

        feedback.fadeOut(5000, function() {
            $(this)
			.html('Saving&hellip;')
			.hide();

            $('.userpanel #status_save').fadeIn(300);
        });
    }

    /** Callback for unsuccessful save of status */
    function fail() {
        feedback.html('Could not be saved!');

        feedback.fadeOut(3000, function() {
            $(this)
			.html('Saving&hellip;')
			.hide();

            $('.userpanel #status_save').fadeIn(300);
        });
    }

    $('.userpanel #status_save')
	.click(function(e) {
	    e.preventDefault();

	    $(this).hide();

	    feedback.show();

	    var data = {};

	    data.memberID = $('.userpanel [name="memberID"]').val();
	    data.statusMessage = $('.userpanel [name="statusMessage"]').val().replace(/(\n|\r)+$/g, '');

	    jsonQuery('/IWCYCAPI.asmx/AjaxSetStatus', data, success, fail);
	});
    // END Ajax save functionality
}

/** initiates Inbox actions - taking standard post inputs and implementing them as request links on the actions panel */
function Inbox() {
    // BGN set up inbox links/actions
    // loop through action button and move to actions panel as links
    $('.inbox .actions input').each(function(i) {
        var subm = $(this);

        var item = $('<li class="' + this.id.replace('inbox_', 'ico_email_') + '"><a href="#"><span>' + this.value + '</span></a></li>');

        item.find('a').click(function(e) {
            e.preventDefault();

            subm.click();
        });

        $('.actionpanel ul').append(item);

        subm.hide();
    })

    // set up 'select all checkboxes' action link
    var sel_all = $('.ico_select_all');

    sel_all.find('a').click(function(e) {
        e.preventDefault();

        $('.inbox :checkbox').each(function(i) {
            this.checked = true;
        });
    });

    // set up 'select none checkboxes' action link
    var sel_none = $('.ico_select_none');

    sel_none.find('a').click(function(e) {
        e.preventDefault();

        $('.inbox :checkbox').each(function(i) {
            this.checked = false;
        });
    });
    // END set up inbox links/actions
}


//// UTILITIES /////////////////////////////////////////////////////////////////////////////////


/**
jQuery extension - performs a conditional equality test on the length of a reult set, only continuing with the jQuery chain if the result is true.
	
@param comparison Object containing any of the following properties with numeric values - gt, gte, lt, lte eq, neq
@example $('li').count({gt:5, lte:10).hide()   // hides all 'li' elements if there is more than 5 of them and less than 11
*/
$.fn.count = function(/**Object*/comparison) {
    var flag = true;
    var isset = false;

    for (var k in comparison) {
        switch (k.toLowerCase()) {
            case 'gt':
                flag = flag && (this.length > comparison[k]);
                isset = true;
                break;

            case 'gte':
                flag = flag && (this.length >= comparison[k]);
                isset = true;
                break;

            case 'lt':
                flag = flag && (this.length < comparison[k]);
                isset = true;
                break;

            case 'lte':
                flag = flag && (this.length <= comparison[k]);
                isset = true;
                break;

            case 'eq':
                flag = flag && (this.length == comparison[k]);
                isset = true;
                break;

            case 'neq':
                flag = flag && (this.length != comparison[k]);
                isset = true;
                break;
        }
    }

    if (isset && flag) {
        return this;
    }
    else {
        return $();
    }
}

/**
jQuery plugin extension which when chained to a jQuery object for an input field, will hide it's label and set the label text as the field value 
with an intelligent* autohide on focus and re-insert on blur
*/
$.fn.labelToValue = function() {
    var self = this;

    self.focus(function() {
        var field = $(this);

        if (field.siblings().hasClass("texthelp") === true) {
            var label = $.trim(field.siblings(".texthelp").hide().html());
        }
        else if (this.id) {
            var label = $.trim($('label[for="' + this.id + '"]').hide().text());
        }
        else {
            return;
        }

        if (field.val() == label) {
            field
			.val('')
			.css('color', '#000');
        }
        else {
            field
			.css('color', '#000');
        }
    })
	.blur(function() {
	    var field = $(this);

	    if ($(this).siblings().hasClass("texthelp") === true) {
	        var label = $.trim($(this).siblings(".texthelp").hide().html());
	    }
	    else if (this.id) {
	        var label = $.trim($('label[for="' + this.id + '"]').hide().text());
	    }
	    else {
	        return;
	    }

	    if (field.val() == label || field.val() == '') {
	        field
			.val(label)
			.css('color', '#999');
	    }
	})
	.blur()
	.parents('form').submit(function(e) {
	    // trigger the focus event to remove label text from field
	    self.focus();
	    // remove all bound events
	    self.unbind();
	});

    return self;
}

/**
jQuery plugin extension which when chained to a jQuery object for an input field, will hide it's label and set the label text as the field value 
with an intelligent* autohide on focus and re-insert on blur
*/
$.fn.hideFieldText = function() {
$(this).each(function() {
        var helpText = $(this).parent().children(':first-child').val();
        if ($(this).val() == "") {
            $(this).val(helpText);
        }
        $(this).focus(function() {
            var field = $(this);
            if (field.hasClass("active") == true) {
                field
                .removeClass("active")
			    .val('');
            }
        })
        $(this).blur(function() {
            var field = $(this);
            if (field.val() == "") {
                if (field.hasClass("active") == false) {
                    field.addClass("active").val(helpText);
                }
            }
            else {
                if (field.hasClass("active") == true) {
                    $(this).removeClass("active");
                }
            }
        })
    });
}

/**
jQuery extension - allows a function to be assigned to a focusable element that will called at teh set interval whenever 
the element is focuesd until the element is 'blurred' 
	
@param func Function to be called at the set interval - this function receives the focused element node as it's only argument
@param [interval] A numeric interval in milliseconds at which to periodically call the supplied function - defaults to 100;
*/
$.fn.pollOnFocus = function(func, interval) {
    if (func) {
        this.data('func', func);

        this.focus(function(e) {
            var self = this;

            $(self).data('interval', setInterval(function() { func(self) }, interval || 100));
        })
		.blur(function() {
		    clearInterval($(this).data('interval'));
		})
    }
    else if (this.data('func')) {
        this.data('func')(this.get(0));
    }

    return this;
}

/**
jQuery extension - will return the caret position in any editable content when called with arguments, and will set teh caret position of any
editable content when a numeric value is passed in as the caret position.
	
@param [position] A numeric value representing the position of the caret within a block of text
*/
$.fn.caret = function(position) {
    var node = this.get(0);

    if (position) {
        if (node.setSelectionRange) {
            node.focus();
            node.setSelectionRange(position, position);
        }
        else if (node.createTextRange) {
            var range = node.createTextRange();
            range.collapse(true);
            range.moveEnd('character', position);
            range.moveStart('character', position);
            range.select();
        }
    }
    else {
        if (node.selectionStart) {
            return node.selectionStart;
        }
        else if (!document.selection) {
            return 0;
        }

        var c = "\001";
        var sel = document.selection.createRange();
        var dul = sel.duplicate();
        var len = 0;

        dul.moveToElementText(node);
        sel.text = c;
        len = (dul.text.indexOf(c));
        sel.moveStart('character', -1);
        sel.text = "";

        return len;
    }
}

/**
Wrapper for the jQuery ajax function to make calls to .Net simpler (by defaulting and auto-serialising certain parameters)
	
@param service URI of web service to call
@param data An object or array (associative) to pass as key=value pairs to the web service via POST
@param success Callback function reference for successful resquest responses
@param fail Callback function reference for unsuccessful request responses
*/
function jsonQuery(/**String*/service, /**Object|Array*/data, /**Function*/success, /**Function*/fail) {
    data = serialize(data);

    // The below is required for correct JSON web services with .Net
    // DO NOT EDIT!
    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: service + "?" + (Math.random() * 999999),
        data: data,
        dataType: "json",
        error: function() {
            if (fail) fail();
        },
        success: function(/**Object*/msg) {
            if (msg.d) {
                if (success) success(msg.d);
            }
            else {
                if (fail) fail();
            }
        }
    });
}

/**
Converts simple objects (non-nested) or associative arrays into string representations of themselves which the web service requires
	
@param data An object or array (associative) to convert into a string representation of itself
*/
function serialize(/**Object|Array*/data) {
    var json = [];

    for (var k in data) {
        json.push(k + ":'" + escape(data[k]) + "'");
    }

    return '{' + json.join(',') + '}';
}

/**
jQuery extension - Hides content (jQuery set) and applies a sliding 'expander' to reveal it when clicked
	
@param loc A jQuery result set for a single element to place the 'expander button' after e.q. $('#content li:last')   -  after the last LI within #content
@param label A string of the required label text for the 'expander button'
*/
$.fn.contentExpander = function(/**jQuery*/loc, /**String*/label, /**Number*/overrideSpeed) {
    if (!this.length) return;
    var set = this;

    set.hide();

    $('<div class="content_expander"><a href="#"><span>' + label + '</span></a></div>')
	.insertAfter(loc)
	.click(function(e) {
	    var expandees = set.wrapAll(
			$('<div class="expandees"></div>')
		)
		.append('<div class="clear">&nbsp;</div>')
		.parent()
		.hide();

	    set.show();

	    var speed = overrideSpeed || Math.min(Math.round(expandees.height() * 3), 3000);

	    e.preventDefault();

	    expandees // relative for IE, and float for Safari
		.slideDown(speed, function() {
		    expandees.replaceWith(expandees.html());
		});

	    $(e.target)
		.parents('.content_expander')
		.fadeOut(Math.round(speed * .6));
	});

    return set;
}

/**
jQuery extension - Create a large clickable area from a hyperlink, when clicked the hyperlink click event is invoked.
	
@param [selector] An optional jQuery selector string pointing the link you wish to 'make big', if ommitted it will use the first hyperlink within the element
*/
$.fn.bigLink = function(/**String*/selector) {
    var link = selector ? $(selector) : this.find('a[@href]:eq(0)');

    this.click(function() {
        // the first link within the supplied element
        var prevent_default = false;

        // check through all click events to for preventDefault command
        var all_events = link.data('events');
        var click_events;

        if (all_events && all_events.click) {
            click_events = all_events.click;

            for (var k in click_events) {
                if (/\.preventDefault\(\)/.test(click_events[k].toString())) {
                    prevent_default = true;
                }
            }
        }

        // trigger all jquery assigned events
        link.triggerHandler('click');

        // if preventDefault has never been assigned to this link then it is safe to do a standard redirect
        if (!prevent_default) {
            document.location = link.attr('href');
        }
    })
	.css('cursor', 'pointer');
}