(function(sdwndw)
{
    sdwndw.defFontSize = 1;
    sdwndw.minFontSize = 0.8;
    sdwndw.maxFontSize = 1.8;
    sdwndw.varWidth = "98";
    sdwndw.fixWidth = "975";
    sdwndw.defWidth = varWidth;
    sdwndw.currentWidth = defWidth;
    sdwndw.currentFontSize = defFontSize;
    sdwndw.cookie_suffix = "3";
    if (location.href.match("/forums/") || location.href.match("/f/"))
    {
        cookie_suffix += "f";
    }

    sdwndw.createCookie = function(name, value, daysorsec, secs)
    {
        var expires;
        var handleCookiesDifferently = ['fpshowexpiredv2', 'fpStyle'];

        if (daysorsec || secs)
        {
            var date = new Date();
            if (secs > 0)
            {
                date.setTime(date.getTime() + (secs * 1000));
            }
            else if (daysorsec > 365)
            {
                date.setTime(date.getTime() + (daysorsec * 1000));
            }
            else
            {
                date.setTime(date.getTime() + (daysorsec * 24 * 60 * 60 * 1000));
            }
            expires = "; expires=" + date.toGMTString();
        }
        else
        {
            expires = "";
        }

        if (handleCookiesDifferently.indexOf(name) != -1)
        {
            $vb_cookie_domain = vb_cookie_domain;
        }

        document.cookie = name + "=" + value + expires + "; path=/" + (typeof $vb_cookie_domain != "undefined" ? "; domain=" + $vb_cookie_domain : "");
    }

    sdwndw.readCookie = function(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;
    }

    sdwndw.setFontSize = function(fontSize)
    {
        $("#topwrapper").css('font-size', fontSize.toString().substr(0, 3) + 'em');
    }

    sdwndw.saveSettings = function()
    {
        createCookie("pageWidth" + cookie_suffix, currentWidth, 365);
        createCookie("fontSize" + cookie_suffix, currentFontSize.toString().substr(0, 3), 365);
    }

    sdwndw.changeFontSize = function(sizeDifference)
    {
        currentFontSize = parseFloat(currentFontSize, 10) + parseFloat(sizeDifference, 10);

        if (currentFontSize > maxFontSize)
        {
            currentFontSize = maxFontSize;
        }
        else if (currentFontSize < minFontSize)
        {
            currentFontSize = minFontSize;
        }
        setFontSize(currentFontSize);
        saveSettings();
    }

    sdwndw.gridResize = function()
    {
        var $dealLists = $("#deal_list, #deal_list_featured");

        $dealLists.each(function(idx, elm)
        {
            var $dealElm = $(elm);

            if ($dealElm.hasClass("grid"))
            {
                $dealElm.css("width", "100%");
                var deals_width = $dealElm.width();
                var columns = Math.floor(deals_width / 150);
                var col_width = Math.floor(deals_width / columns) - 2;
                $dealElm.css("width", deals_width);
                $dealElm.find(".dealitem").css("width", col_width);

                var offsets = [];
                var $list = $dealElm.find(".dealitem:visible");

                $list.each(function()
                {
                    offsets.push($(this).offset().top);
                });

                $.each(offsets, function(i, v)
                {
                    if (v < offsets[i + 1] || typeof offsets[i + 1] == "undefined")
                    {
                        $list.eq(i).css('border-right', '1px dotted #999');
                    }
                    else
                    {
                        $list.eq(i).css('border-right', 'none');
                    }
                });
            }
        });
    }

    sdwndw.showStyleHelp = function()
    {
        var tipcontent = "<a class='notice_close' style='position:absolute;top:-1px;right:-3px;cursor:pointer;' ";
        tipcontent += "onclick='hideStyleHelp()'></a>Try out the new display options!";
        $("#fp_style_list").bt(tipcontent, {
            fill: "#FDF4B5",
            positions: ['top'],
            trigger: 'none',
            clickAnywhereToClose: false,
            cssStyles: {
                "font-size": "13px",
                "font-weight": "bold",
                "text-align": "center",
                "cursor": "default",
                "color": "black",
                "background": "url(https://js.slickdealscdn.com/images/slickdeals/gradients/white_up.png) repeat-x bottom left"
            },
            shrinkToFit: false,
            strokeStyle: "#FDD33C",
            shadow: true,
            width: "130px"
        }).btOn();
    }

    sdwndw.hideStyleHelp = function()
    {
        if ($(".bt-content .notice_close").is(":visible"))
        {
            $("#fp_style_list").btOff();
            createCookie("hidestylehelp", 1, 365);
        }
    }

    sdwndw.setWidth = function(width)
    {
        if (width == fixWidth)
        {
            $("#topwrapper").width(fixWidth + "px");
            $("body").removeClass("varwidth").addClass("fixwidth");
        }
        else if (width == varWidth)
        {
            $("#topwrapper").width(varWidth + "%");
            $("body").removeClass("fixwidth").addClass("varwidth");
        }
        currentWidth = width;
        saveSettings();
        if (typeof gridResize != "undefined")
        {
            gridResize();
        }
        if (typeof storepageResize != "undefined")
        {
            storepageResize();
        }
        if (typeof bffeaturedeal_resize == 'function')
        {
            bffeaturedeal_resize();
        }
        if ('update_width_dynamic' in window)
        {
            update_width_dynamic();
        }
    }

    sdwndw.revertStyles = function()
    {
        currentWidth = defWidth;
        setWidth(defWidth);

        currentFontSize = defFontSize;
        changeFontSize(0);
    }

    $(document).ready(function()
    {
        gridResize();
        $(window).resize(gridResize);
        if ($("#topwrapper").width() == fixWidth)
        {
            $("body").removeClass("varwidth").addClass("fixwidth");
        }
        else
        {
            $("body").removeClass("fixwidth").addClass("varwidth");
        }

        var $expiredCells = $('.expiredbutton') || null;
        if ($expiredCells != null && !$expiredCells.is(':visible'))
        {
            $expiredCells.trigger('click');
        }
    });

    sdwndw.toggleWidth = function()
    {
        if (currentWidth == fixWidth)
        {
            currentWidth = varWidth;
        }
        else
        {
            currentWidth = fixWidth;
        }
        setWidth(currentWidth);

        if (typeof update_width_dynamic == "function")
        {
            update_width_dynamic();
        }
    }

    /** Since media queries don't support element relative bits... (Derek) **/

    sdwndw.update_width_debounce = function ()
    {
        if (!('__width_dyn__' in window))
        {
            return false;
        }

        if ('__update_width_debounce' in window)
        {
            clearTimeout(window.__update_width_debounce);
        }
        window.__update_width_debounce = setTimeout(update_width_dynamic, 25);
    }

    sdwndw.fetch_width_dynamic = function ()
    {
        var e, o, i, l, evt,  w = window;

        // Exit if we already have the list built. - also deny old IE
        if ('__width_dyn__' in window)
        {
            return false;
        }

        // Listener
        if ('addEventListener' in window)
        {
            l = window.addEventListener;
            evt = 'resize';
        }
        else if ('attachEvent' in window)
        {
            l = window.attachEvent;
            evt = 'onresize';
        }
        else
        {
            return false;
        }

        // Init list and iterate
        w.__width_static__ = 0;
        w.__width_dyn__ = [];

        // Get width of current pieces already in submenu
        $('#submenu a:not(.width_dynamic)').each(function () {
            w.__width_static__ += $(this).outerWidth(true);
        });

        if (!('getElementsByClassName' in document))
        {
            document.getElementsByClassName = function (cls) {
                return $('.' + cls);
            };
        }

        e = document.getElementsByClassName('width_dynamic');

        // Only attach event listener if we have dynamic bits to change
        if (e.length > 0)
        {
            for (i = 0; i < e.length; i++)
            {
                w.__width_dyn__.push([e[i],$(e[i]).outerWidth(true)]);
            }

            l(evt, update_width_debounce, false);
        }
    }

    sdwndw.update_width_dynamic = function ()
    {

        var i, l, m, available,
            width = parseInt($('#submenu').width()),
            w = window.__width_dyn__,
            base_width = window.__width_static__;

        available = width - base_width;

        if (w[0])
        {
            l = w.length;

            for (i = 0; i < l; i++)
            {
                m = (available - w[i][1] > 0) ? 'block' : 'none';
                available -= m === 'block' ? w[i][1] : 0;

                // Reduce unnecessary assignments or possible repaints
                if (w[i][0].style.display !== m)
                {
                    w[i][0].style.display = m;
                }
            }
        }
    }
})(window);

;if (typeof DISPLAY_NOJS == "undefined")
{
    window.isModal = /modal=[^0.]/.test(window.location.search);

    // Variables we use throughout the application
    window.FACEBOOK_OFF = false;

    window.sdfacebook = window.sdfacebook || {}; // global namespace for slickdeals js facebook
    sdfacebook.read_permissions = ['email'];
    sdfacebook.write_permissions = [];
    sdfacebook.permissions = sdfacebook.read_permissions.concat(sdfacebook.write_permissions);
    sdfacebook.profile = {};
    sdfacebook.fb_message = "";

    // We set this in the code of the header via the users options from the database
    sdfacebook.fb_options = sdfacebook.fb_options || 0;

    sdfacebook.EVERYTHING_OFF = 4026531840;

    // comma separated list of fields we want to pull from the facebook users profile
    sdfacebook.profile_fields = "first_name,last_name,birthday,gender,email,location";

    // boolean flag sets true when user is logged in on slickdeals
    sdfacebook.SDLoggedIn = sdfacebook.SDLoggedIn || false;
    sdfacebook.AccessToken = "";
    sdfacebook.usingDefaultOptions = false;

    sdfacebook.complete_signup = false;
    sdfacebook.chosen_username = '';
    sdfacebook.newsletter = 0;
    sdfacebook.need_permission_rerequest = false;
    sdfacebook.actionSource = null;
    sdfacebook.regUrl = '';
    sdfacebook.autoReg = false;
    sdfacebook.forceUsername = true;
    sdfacebook.sdkLoaded = false;
    sdfacebook.registrationTracking = {};

    // Init the SDK upon load
    window.fbAsyncInit = function()
    {
        if (typeof FB != "undefined")
        {
            FB.init({
                appId      : facebook_appid, // Production appid
                status     : false, // check login status
                cookie     : true, // enable cookies to allow the server to access the session
                xfbml      : false,// parse XFBML
                version    : 'v6.0'
            });

            // listen for and handle auth.statusChange events
            FB.Event.subscribe('auth.statusChange', sdfacebook.FacebookInit);

            sdfacebook.sdkLoaded = true;

            $(document).ready(function ()
            {
                sdfacebook.initializeFacebookElements();
            });
        }
        else
        {
            FACEBOOK_OFF = true;
        }

        $(document).on('click', '[data-role="facebookSignUp"]', sdfacebook.signUpClickHandler);
    };

    sdfacebook.isExtension = function()
    {
        return window.dataLayer.visitor.layout === 'Extension';
    }

    sdfacebook.getWindowParent = function ()
    {
        return sdfacebook.isExtension() ? window : window.parent;
    };

    sdfacebook.signUpClickHandler = function (event)
    {
        var $elm = $(event.currentTarget);
        var loginOptions = $elm.data('login-options') || {};

        sdfacebook.trackAttempt(loginOptions.regLogForm);

        sdfacebook.SingleSignon($elm.data('login-options'));
    };

    sdfacebook.trackAttempt = function (regLogForm)
    {
        if (window.SD && window.SD.RegLog && regLogForm)
        {
            sdfacebook.registrationTracking = window.SD.RegLog.socialAttempt(regLogForm, 'facebook');
        }
    };

    sdfacebook.getSDKLoadPriority = function ()
    {
        var criticalPathLocations = [
            '/forums/register.php',
            '/forums/login.php'
        ];

        return criticalPathLocations.indexOf(window.location.pathname) > -1 ? 'criticalPath' : 'nonCriticalUi';
    };

    $(document).ready(function () {
        // Load the SDK Asynchronously
        window.sdLoadQueue.push([function() {
            (function(d){
                var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
                if (d.getElementById(id))
                {
                    return;
                }
                js = d.createElement('script');
                js.id = id;
                js.async = true;
                js.src = "//connect.facebook.net/en_US/sdk.js";
                ref.parentNode.insertBefore(js, ref);
            }(document));
        }, sdfacebook.getSDKLoadPriority()]);

        if (sdfacebook.fb_options == sdfacebook.EVERYTHING_OFF)
        {
            FACEBOOK_OFF = true;
        }

        // Click the link facebook button from account settings page
        $("#link_facebook").click(function (e) {
            sdfacebook.FacebookLogin(function () {
                location.reload();
            }, true);
            e.preventDefault();
            return false;
        });

        // creates div for facebook dialog
        if ($("#sdfacebook_options_dialog").length == 0)
        {
            $('body').append("<div id='sdfacebook_options_dialog'></div>");
        }

        if (isModal)
        {
            $('#linkaccount_facebook form').submit(sdfacebook.handleDoLinkSubmit);
            $('#choose_username_facebook').submit(sdfacebook.handleChooseUsernameSubmit);
        }
    });

    sdfacebook.initializeFacebookElements = function()
    {
        $('[data-role="facebookSignUp"]').prop('disabled', false);

        sdfacebook.xfbmlParse();
    };

    sdfacebook.xfbmlParse = function ()
    {
        var fbElements = [
            '#fbLike',
            'div[data-facebook-sign-up-container]'
        ];

        fbElements.forEach(function (selector)
        {
            $(selector).each(function (idx, elm)
            {
                FB.XFBML.parse(elm);
            });
        });
    };

    sdfacebook.CollapseDialogOptions = function()
    {
        $('#fbchangeLink').text("Don't Ask Me Again");

        $('#fbchangeLink').unbind('click');
        $('#fbchangeLink').click(function()
        {
            sdfacebook.ExpandDialogOptions();
        });

        $('.fbPreviewContainer.activity').removeClass("hide");
        $('.fbPreviewContainer.settings').addClass('hide');
    };

    // This will remove the app from the users permissions
    sdfacebook.DeleteApp = function(access_token)
    {
        FB.api('/me/permissions', 'delete', {
            access_token: access_token
        });
    };

    sdfacebook.DialogDefaultRenderCheck = function()
    {
        if (sdfacebook.usingDefaultOptions == true)
        {
            $('#optionChange').val('1');
            $('input[name="optionChange"]').val('1');
        }
    };

    sdfacebook.ExpandDialogOptions = function()
    {
        $('#fbchangeLink').text("Save Settings");

        $('#fbchangeLink').unbind('click');
        $('#fbchangeLink').click(function()
        {
            sdfacebook.CollapseDialogOptions();
        });

        $('.fbPreviewContainer.activity').addClass("hide");
        $('.fbPreviewContainer.settings').removeClass('hide');
    };

    // Runs every time the page loads if user is logged into facebook, so be careful what goes in here
    sdfacebook.FacebookInit = function(response)
    {
        if (response.status != "not_authorized" && response.status != "unknown")
        {
            // They click unlink from account settings
            $("#unlinkFacebook").click(function(e){
                e.preventDefault();
                if (sdfacebook.UnlinkFacebook(response.authResponse.accessToken))
                {
                    location.reload();
                }
            });

            sdfacebook.AccessToken = response.authResponse.accessToken;
            sdfacebook.FacebookId = response.authResponse.userID;
        }
    };

    sdfacebook.LinkFacebook = function (facebookId, accessToken, callback){
        var passwordModal = new PasswordModal({
            headline: 'Please enter your password',
            message: 'This action requires you to enter your Slickdeals password.',
            onSubmit: function (password)
            {
                var settings = {
                    type: 'post',
                    data: {
                        do: 'link_facebook',
                        securitytoken: SECURITYTOKEN,
                        password: password,
                        facebookid: facebookId,
                        access_token: accessToken,
                    },
                    url: '/ajax/profile_ajax.php',
                    dataType: 'json'
                };

                $.ajax(settings).done(function (response)
                {
                    passwordModal.close();

                    if (response.error)
                    {
                        new ErrorModal(response.error);
                    }
                    else if (typeof callback === 'function')
                    {
                        callback();
                    }
                    else
                    {
                        // Fallback just in case callback function not defined.
                        window.location.reload();
                    }
                });
            }
        });
    };


    sdfacebook.LinkFacebookClassic = function (facebookId, accessToken, callback)
    {
        var html = 'This action requires you to enter your Slickdeals.net password.<br />' +
                '<input type="password" name="password" id="link_facebook_password" />';

        var okfunc = function ()
        {
            var password = $('#link_facebook_password').val();
            $.post('/ajax/profile_ajax.php',
                {
                    do: 'link_facebook',
                    securitytoken: SECURITYTOKEN,
                    password: password,
                    facebookid: facebookId,
                    access_token: accessToken,
                },
                function (data)
                {
                    if (data.error)
                    {
                        var errorokfunc = function ()
                        {
                            if (typeof callback === 'function')
                            {
                                callback();
                            }
                        };

                        genericdialog('Error', data.error, null, errorokfunc);
                    }
                    else if (typeof callback === 'function')
                    {
                        callback();
                    }
                    else
                    {
                        // Fallback just in case callback function not defined.
                        window.location.reload();
                    }
                },
                'json'
            );
        };

        genericdialog('Please enter your password', html, null, okfunc);
        $('#link_facebook_password').focus().keypress(function (e)
        {
            if (e.keyCode === 13)
            {
                $(e.target).closest('.ui-dialog').find('.ui-dialog-buttonpane button').click();
            }
        });
    };

    sdfacebook.checkHasGrantedAllLoginPermissions = function(successCallback, failCallback){
        FB.api('/me/permissions?fields=permission,status', function(response){
            if (!sdfacebook.hasGrantedAllLoginPermissions(response))
            {
                return failCallback();
            }
            else
            {
                return successCallback();
            }
        });
    };

    sdfacebook.hasGrantedAllLoginPermissions = function(response) {
        if (typeof response !== 'object' || !response.hasOwnProperty('data') || !$.isArray(response.data))
        {
            return false;
        }

        for (var i in response.data)
        {
            if (!isNaN(parseInt(i)))
            {
                var permission = response.data[i];
                if (!permission.hasOwnProperty('status') || permission.status !== 'granted')
                {
                    // permission hasn't been granted
                    return false;
                }
            }
        }

        return true;
    };

    sdfacebook.FacebookLoginCallback = function(r, callback, nosave)
    {
        // if error, user canceled or closed window
        if (!r.authResponse)
        {
            return false;
        }

        var handleUserHasGrantedLoginPermissions = function(){
            sdfacebook.handleUserHasGrantedLoginPermissions(r, callback, nosave)
        };

        sdfacebook.checkHasGrantedAllLoginPermissions(
            handleUserHasGrantedLoginPermissions,
            sdfacebook.handleUserHasNotGrantedAllLoginPermissions
        );
        return true;
    };

    sdfacebook.handleUserHasGrantedLoginPermissions = function(r, callback, nosave) {
        if (sdfacebook.SDLoggedIn)
        {
            if (nosave && r.authResponse.userID && r.authResponse.accessToken)
            {
                if (window.dataLayer.visitor.layout === 'Classic')
                {
                    sdfacebook.LinkFacebookClassic(r.authResponse.userID, r.authResponse.accessToken, callback);
                }
                else
                {
                    sdfacebook.LinkFacebook(r.authResponse.userID, r.authResponse.accessToken, callback);
                }
            }
        }
        else if (typeof callback === "function")
        {
            callback();
            return true;
        }
        return false;
    };

    sdfacebook.handleUserHasNotGrantedAllLoginPermissions = function() {
        sdfacebook.need_permission_rerequest = true;
        window.parent.errordialog('You must grant all Facebook permissions to login with Facebook. Please login again and accept those permissions to login with Facebook');
        return false;
    };

    sdfacebook.FacebookLogin = function(callback, nosave, write_perms)
    {
        var request_permissions = [];
        request_permissions.push(sdfacebook.read_permissions);

        if (write_perms)
        {
            request_permissions.push(sdfacebook.write_permissions);
        }

        var params = {
            scope: request_permissions.join(",")
        };

        if (sdfacebook.need_permission_rerequest)
        {
            params.auth_type = 'rerequest';
        }

        if (typeof FB != "undefined")
        {
            // logs them in, and fetches permissions
            FB.login(function(r) {
                sdfacebook.FacebookLoginCallback(r, callback, nosave);
            }, params);
        }
    };

    sdfacebook.SaveOptions = function(options, callback)
    {
        if (options != sdfacebook.fb_options)
        {
            $.post("/ajax/profile_ajax.php",
                {
                    "do"            : "update_fb_options",
                    "securitytoken" : SECURITYTOKEN,
                    "fb_options"    : options
                },
                function(data)
                {
                    if (data.success)
                    {
                        sdfacebook.fb_options = options;
                    }

                    if (typeof callback === "function")
                    {
                        callback();
                    }
                },
                "json"
            );
        }
        else
        {
            if (typeof callback === "function")
            {
                callback();
            }
        }
    };

    // Does login for slickdeals via facebook
    sdfacebook.SlickdealsLogin = function()
    {
        var params = {
            'securitytoken': SECURITYTOKEN,
            'do': 'facebook',
            'access_token': sdfacebook.AccessToken,
            'complete_signup': sdfacebook.complete_signup,
            'chosen_username': sdfacebook.chosen_username,
            'forceUsername': sdfacebook.forceUsername,
            'newsletter': sdfacebook.newsletter,
            'modal': isModal ? 1 : 0,
            'action_source': sdfacebook.actionSource,
            'regUrl': sdfacebook.regUrl,
            'autoReg': sdfacebook.autoReg,
            'goToRegUrl': sdfacebook.goToRegUrl,
            'registrationTracking': sdfacebook.registrationTracking
        };

        if ($("input[name=fbuid]").length > 0)
        {
            params['fbuid'] = $("input[name=fbuid]").val();
        }

        $.post('/forums/sdlogin.php', params, function(data, x, request)
        {
            if ($(data).find('retry').length > 0)
            {
                sdfacebook.FacebookLogin(function(){
                    sdfacebook.SlickdealsLogin();
                });
            }
            else if ($(data).find('error').length > 0)
            {
                if (params.complete_signup)
                {
                    SD.Analytics.signUpTrack('unsuccessful');
                }
                else
                {
                    SD.Analytics.loginTrack('unsuccessful');
                }
                //window.parent.location = $(data).find('goto').text();
                if (typeof sdfacebook.getWindowParent().errordialog != 'undefined')
                {
                    sdfacebook.getWindowParent().errordialog($(data).find('error').text(), false, 200);
                }
                else if ($('#regform_errormessage').length > 0)
                {
                    $('#regform_errormessage').html($(data).find("error").text()).show();
                }
            }
            else if ($(data).find('success').length > 0)
            {
                if ($(data).find('goto').length > 0)
                {
                    sdfacebook.getWindowParent().location = $(data).find('goto').text();
                }
                else if (sdfacebook.postRedirect)
                {
                    sdfacebook.getWindowParent().location = sdfacebook.regUrl ? sdfacebook.regUrl : '/';
                }
                // for redesign
                else if (sdfacebook.goToRegUrl && sdfacebook.regUrl)
                {
                    sdfacebook.getWindowParent().location = sdfacebook.regUrl;
                }
                else if (sdfacebook.isExtension())
                {
                    window.location = sdfacebook.regUrl;
                }
                else
                {
                    sdfacebook.getWindowParent().location.reload();
                }
            }
            else if ($(data).find('sdsso_link').length > 0)
            {
                sdfacebook.initSingleSignOnLink(data);
            }
            else if ($(data).find('checkusername').length > 0)
            {
                if (sdfacebook.skipChangeUsername)
                {
                    sdfacebook.getWindowParent().location = sdfacebook.regUrl;
                }
                else
                {
                    sdfacebook.initChooseUsername(data);
                }
            }
            else
            {
                sdfollowers.RecommendedFriendsDialog(data);
            }
        });
    };

    sdfacebook.initSingleSignOnLink = function(data){
        // hack to parse html object, since $.parseHTML isn't available at the time of writing
        var html = $('<html />').html($(data).find('sdsso_link').text());
        var socialEmail = html.find('input[name="vb_login_username"]').val();
        var socialName = html.find('input[name="fb_name"]').val();
        var username = html.find('input[name="vb_username"]').val();
        var accessToken = html.find('input[name="access_token"]').val();
        var actionSource = html.find('input[name="action_source"]').val();
        var regUrl = html.find('input[name="regUrl"]').val();

        var params = {
            'do': 'sso-link',
            'type': 'facebook',
            'username': username,
            'socialEmail': socialEmail,
            'socialName': socialName,
            'access_token': accessToken,
            'action_source': actionSource,
            'regUrl': regUrl,
            'auto': sdfacebook.autoReg,
            'goToRegUrl': sdfacebook.goToRegUrl,
            'securitytoken': SECURITYTOKEN
        };

        var url = '/forums/login.php';

        if (isModal)
        {
            url += '?modal=1';
        }

        if (sdfacebook.autoReg)
        {
            url += '?auto=1';
        }
        postLocationRedirect(url, params);
    };

    // Logs into facebook, then does the login for slickdeals
    sdfacebook.SingleSignon = function(loginArgs)
    {
        loginArgs = loginArgs || {};

        sdfacebook.setLoginArgProperties(loginArgs);

        if ($('#loginbox_signin_ajax'))
        {
            $('#loginbox_signin_ajax').css('display', 'inline-block');
            $('#loginbox_signin_ajax > img').attr('src', '/images/slickdeals/ajaxsmall.gif');
        }
        sdfacebook.FacebookLogin(function(){
            sdfacebook.SlickdealsLogin();
        });
    };

    // Callback method for the official facebook login button.
    // Also executed when someone dismisses the sign in modal without using it, which we consider an attempt since we can't
    //  listen for clicks on the button
    sdfacebook.onButtonLogin = function(loginArgs)
    {
        loginArgs = loginArgs || {};

        sdfacebook.setLoginArgProperties(loginArgs);

        sdfacebook.trackAttempt(loginArgs.regLogForm);

        sdfacebook.SlickdealsLogin();
    };

    sdfacebook.setLoginArgProperties = function(loginArgs)
    {
        sdfacebook.actionSource = loginArgs.actionSource;
        sdfacebook.regUrl       = loginArgs.regUrl;
        sdfacebook.autoReg      = loginArgs.autoReg || false;
        sdfacebook.goToRegUrl   = loginArgs.goToRegUrl || false;
        sdfacebook.skipChangeUsername = loginArgs.skipChangeUsername || false;
    };

    // Unlinks the current account from facebook, and deauthorizes the app
    sdfacebook.UnlinkFacebook = function(access_token)
    {
        $.post("/ajax/profile_ajax.php",
            {
                "do"            : "unlink_facebook",
                "securitytoken" : SECURITYTOKEN
            },
            function(data)
            {
                if (data.error)
                {
                    errordialog(data.error, false, 200);
                    return false;
                }
                else
                {
                    if (typeof(access_token) != "undefined")
                    {
                        sdfacebook.DeleteApp(access_token);
                        return true;
                    }
                    return false;
                }
            },
            "json"
        );
    };

    // Updates the facebook id on the users account, and immediately gets an extended auth token
    sdfacebook.UpdateFacebookId = function(fbid, access_token, profile, callback)
    {
        $.post("/ajax/profile_ajax.php",
            {
                "do"            : "link_facebook",
                "facebookid"    : fbid,
                "access_token"  : access_token,
                'securitytoken' : SECURITYTOKEN,
            },
            function(data)
            {
                if (data.error != false)
                {
                    if (window.dataLayer.visitor.layout == 'Classic')
                    {
                        function errorokfunc()
                        {
                            if (typeof callback == "function")
                            {
                                callback();
                            }
                        }

                        genericdialog("Error", data.error, null, errorokfunc);
                    }
                    else
                    {
                        new ErrorModal(data.error);
                    }
                }
                else
                {
                    if (typeof callback == "function")
                    {
                        callback();
                    }
                }
            },
            'json'
        );
    };

    sdfacebook.initChooseUsername = function(data){
        // hack to parse html object, since $.parseHTML isn't available at the time of writing
        var html = $('<html />').html($(data).find('checkusername').text());

        var username = html.find('input[name="vb_login_username"]').val();

        var url = '/forums/login.php?do=social-complete';

        if (isModal)
        {
            url += '&modal=1';
        }

        if (sdfacebook.regUrl)
        {
            url += '&url=' + encodeURIComponent(sdfacebook.regUrl);
        }

        if (sdfacebook.autoReg)
        {
            $('.autoRegisterModal').hide();
            require(['sd/registration/autoRegistrationUsernameModal'], function(AutoRegistrationUsernameModal){
                new AutoRegistrationUsernameModal({
                    accessToken: sdfacebook.AccessToken,
                    securityToken: SECURITYTOKEN,
                    actionSource: sdfacebook.actionSource,
                    regUrl: sdfacebook.regUrl,
                    username: username,
                    type: 'facebook'
                }).show();
            });
        }
        else
        {
            location.href = url;
        }
    };

    sdfacebook.initChooseUsernameMobile = function(data, usernameForm)
    {
        var form = data ? $($(data).find('checkusername').text()) : usernameForm, doc = $(document), overlay;

        if ($("#loginbox_overlay").length > 0)
        {
            $("#loginbox_overlay").remove();
        }

        $('body').append(form);

        $("#loginbox_overlay").show();

        $("#popup_dialog_newsletter_link").click(function () {
            $('#dialog_newsletter').dialog('open');
            return false;
        });

        // Attach events
        overlay = $('#loginbox_overlay');
        overlay.css('height', doc.height() + 'px');     // Set overlay to height of page
        overlay.click(function () {$(this).hide()});    // Hide form if overlay is clicked

        form = $('div.loginbox_container', overlay);
        form.click(function (e) {e.stopPropagation()}); // Allow clicks on form to prevent hiding stuff

        $('#loginbox_overlay').click(function(elem){
            if ($(elem.target).attr('id') == 'loginbox_overlay')
            {
                overlay.remove();
                $('#loginbox_signin_ajax').hide();
            }
        });
        $('.meh', form).click(function () {
            overlay.remove();
            $('#loginbox_signin_ajax').hide();
        }); // Allow hiding on a "never mind"

        $('#regform_choose_username', form).focus(); // Foooocus!

        $('#regform_choose_username').keyup(function(){
            $('#regform_submit').hide();
        });

        $('#regform_submit').prop('disabled', false);

        $('#regform_checkusername').click(function(){
            $('#regform_username_status > img').attr('src', '/images/slickdeals/ajaxsmall.gif');
            $('#regform_username_status').css('display', 'inline-block');

            var username = $('#regform_choose_username').val();

            sdfacebook.checkUsername(username, function(data){
                var source =  $('#regform_checkusername').data('source');
                if (data.form)
                {
                    $('body').append(data.form);
                }
                else
                {
                    if (data.error == true)
                    {
                        SD.Analytics.signUpTrack('unsuccessful');
                        if (source == "mobile3")
                        {
                            $('#regform_submit').hide();
                            $('#regform_username_status').html('Username has already been registered&hellip;')
                                .removeClass('success')
                                .addClass('failure')
                                .show();
                        }
                        else
                        {
                            $('#regform_submit').hide();
                            $('#regform_errormessage').html(data.message);
                            $('#regform_username_status > img').attr('src', '/images/slickdeals/cross.png');
                        }
                    }
                    else
                    {
                        if (source == "mobile3")
                        {
                            $('#regform_username_status').html('Username is available!')
                                .removeClass('failure')
                                .addClass('success')
                                .show();
                            $('#regform_submit').show();
                        }
                        else
                        {
                            $('#regform_errormessage').html('');
                            $('#regform_submit').show();
                            $('#regform_username_status > img').attr('src', '/images/slickdeals/tick.png');
                        }
                    }
                }
            });
            return false;
        });

        function handleChooseUserSubmitFacebook() {
            if (this.requireJS)
            {
                sdfacebook = this.sdfacebook;
            }

            if ($('.loginbox_form_container').length > 0)
            {
                $('.loginbox_form_container').block({
                    message: null,
                    overlayCSS:  {
                        backgroundColor: '',
                        opacity: 'inherit',
                        timeout: 5000
                    }
                });
            }

            sdfacebook.complete_signup = true;
            sdfacebook.chosen_username = $('#regform_choose_username').val();
            sdfacebook.emitSocialEvent = false;

            if ($('#chk_newsletter').is(':checked') == true)
            {
                sdfacebook.newsletter = 1;
            }
            else
            {
                sdfacebook.newsletter = 0;
            }
            sdfacebook.SlickdealsLogin();
        }

        $('#loginbox_overlay.loginbox_overlay_chooseuser #regform_submit').click(function() {
            handleChooseUserSubmitFacebook();
        });
    };

    sdfacebook.checkUsername = function(username, callback)
    {
        var params = {};
        params['do'] = 'check_username';
        params['securitytoken'] = window.SECURITYTOKEN;
        params['username'] = username;
        params['action_source'] = sdfacebook.actionSource;
        params['regUrl'] = sdfacebook.regUrl;

        $.ajax({
            url: '/forums/sdlogin.php',
            cache: false,
            data: params,
            dataType: 'json',
            type: 'POST',
            success: function(data){
                callback(data);
            }
        });
    };

    // mobile only, used for handling redirects that need to show a certain form
    sdfacebook.initFormHandlers = function(formType, actionSource, regUrl)
    {
        // attach js to form elements
        if (formType == 'chooseUsername')
        {
            sdfacebook.initChooseUsernameMobile(false, $('#loginbox_overlay'));
        }
        else if (formType == 'ssoLink')
        {
            sdfacebook.initMobileSsoLinkForm($('#loginbox_overlay'));
        }
        // initialization done in SingleSignOn()
        sdfacebook.actionSource = actionSource;
        sdfacebook.regUrl = sdfacebook.isExtension() ? regUrl : '/';
        sdfacebook.goToRegUrl = true;
        // don't reload on post redirect because it causes security token errors
        sdfacebook.postRedirect = true;
    };

    sdfacebook.initMobileSsoLinkForm = function($overlay)
    {
        var $form = $('#sdssoForm');

        if ($overlay.length > 0)
        {
            $('#regError').show();
            $overlay.show();

            // Attach events
            $overlay.click(function(e) {
                $(e.target).hide();
            }); // Hide form if overlay is clicked

            $form.click(function(e) {
                e.stopPropagation();
            }); // Allow clicks on form to prevent hiding stuff

            $('#regform_password', $form).focus(); // Foooocus!

            $form.submit(facebookSSOLinkTracking);
        }
    };

    sdfacebook.handleDoLinkSubmit = function(ev){
        // this will hijack the form submission
        // and make it an ajax call instead
        // this allows us to handle refreshing the page after the user logs in
        ev.preventDefault();

        var requestUrl = $(ev.currentTarget).attr('action');
        var params = {};

        if (isModal)
        {
            params['modal'] = 1;
        }

        $(ev.currentTarget).find('input[name]').each(function(index){
            // get all the input fields and load it into a params
            params[$(this).attr('name')] = $(this).val();
        });

        $.post(requestUrl, params, function(data)
            {
                // TODO: Handle any additional issues
                if ($(data).find('successful_link').length > 0)
                {
                    if (params.goToRegUrl && params.regUrl)
                    {
                        sdfacebook.getWindowParent().location = params.regUrl;
                    }
                    else
                    {
                        sdfacebook.getWindowParent().location.reload();
                    }
                }
            }
        );
    };

    sdfacebook.handleChooseUsernameSubmit = function(ev){
        ev.preventDefault();

        var $currentTarget = $(ev.currentTarget);

        // setup the call
        sdfacebook.AccessToken = $currentTarget.find('input[name="access_token"]').val();
        sdfacebook.complete_signup = true;
        sdfacebook.chosen_username = $currentTarget.find('input[name="chosen_username"]').val();
        sdfacebook.newsletter = $currentTarget.find('input[name="newsletter"]').prop('checked') ? 1 : 0;
        sdfacebook.actionSource = $currentTarget.find('input[name="action_source"]').val();
        sdfacebook.regUrl = $currentTarget.find('input[name="regUrl"]').val();
        sdfacebook.emitSocialEvent = false;

        // call the parent one to handle the ajax
        sdfacebook.SlickdealsLogin();
    };

    window.elementSupportsAttribute = function (element, attribute)
    {
        var test = document.createElement(element);
        return attribute in test;
    };

    window.facebookSSOLinkTracking = function ()
    {
        params = $('#sdssoForm').serialize();
        $.ajax({
            url: '/forums/sdlogin.php',
            cache: false,
            data: params,
            dataType: 'json',
            type: 'POST',
            success: function(data) {
                if (data.successful_link == true)
                {
                    if (sdfacebook.isExtension())
                    {
                        location.href = sdfacebook.regUrl;
                    }
                    else
                    {
                        // redirect to fp
                        location.href = "/";
                    }
                }
            }
        });

        return false;
    };
}

;// jQuery Context Menu Plugin
//
// Version 1.00
//
// Cory S.N. LaViska
// A Beautiful Site (http://abeautifulsite.net/)
//
// Visit http://abeautifulsite.net/notebook/80 for usage and more information
//
// Terms of Use
//
// This software is licensed under a Creative Commons License and is copyrighted
// (C)2008 by Cory S.N. LaViska.
//
// For details, visit http://creativecommons.org/licenses/by/3.0/us/
//
if(jQuery)( function() {
	$.extend($.fn, {
		
		contextMenu: function(o, callback) {
			// Defaults
			if( o.menu == undefined ) return false;
			if( o.inSpeed == undefined ) o.inSpeed = 150;
			if( o.outSpeed == undefined ) o.outSpeed = 75;
			// 0 needs to be -1 for expected results (no fade)
			if( o.inSpeed == 0 ) o.inSpeed = -1;
			if( o.outSpeed == 0 ) o.outSpeed = -1;
			// Loop each context menu
			$(this).each( function() {
				var el = $(this);
				var offset = $(el).offset();
				// Add contextMenu class
				$('#' + o.menu).addClass('contextMenu');
				// Simulate a true right click
				$(this).click( 
					function(e) {
					var evt = e;
						var srcElement = $(this);
						$(this).unbind('mouseup');
							// Hide context menus that may be showing
							$(".contextMenu").hide();
							// Get this context menu
							var menu = $('#' + o.menu);
							
							if( $(el).hasClass('disabled') ) return false;
							
							// Detect mouse position
							var d = {}, x, y;
							if( self.innerHeight ) {
								d.pageYOffset = self.pageYOffset;
								d.pageXOffset = self.pageXOffset;
								d.innerHeight = self.innerHeight;
								d.innerWidth = self.innerWidth;
							} else if( document.documentElement &&
								document.documentElement.clientHeight ) {
								d.pageYOffset = document.documentElement.scrollTop;
								d.pageXOffset = document.documentElement.scrollLeft;
								d.innerHeight = document.documentElement.clientHeight;
								d.innerWidth = document.documentElement.clientWidth;
							} else if( document.body ) {
								d.pageYOffset = document.body.scrollTop;
								d.pageXOffset = document.body.scrollLeft;
								d.innerHeight = document.body.clientHeight;
								d.innerWidth = document.body.clientWidth;
							}
							(e.pageX) ? x = e.pageX : x = e.clientX + d.scrollLeft;
							(e.pageY) ? y = e.pageY : x = e.clientY + d.scrollTop;
							
							// Show the menu
							$(document).unbind('click');
							$(menu).css({ top: y, left: x }).fadeIn(o.inSpeed);
							// Hover events
							$(menu).find('A').mouseover( function() {
								$(menu).find('LI.hover').removeClass('hover');
								$(this).parent().addClass('hover');
							}).mouseout( function() {
								$(menu).find('LI.hover').removeClass('hover');
							});
							
							// Keyboard
							$(document).keypress( function(e) {
								switch( e.keyCode ) {
									case 38: // up
										if( $(menu).find('LI.hover').size() == 0 ) {
											$(menu).find('LI:last').addClass('hover');
										} else {
											$(menu).find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover');
											if( $(menu).find('LI.hover').size() == 0 ) $(menu).find('LI:last').addClass('hover');
										}
									break;
									case 40: // down
										if( $(menu).find('LI.hover').size() == 0 ) {
											$(menu).find('LI:first').addClass('hover');
										} else {
											$(menu).find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover');
											if( $(menu).find('LI.hover').size() == 0 ) $(menu).find('LI:first').addClass('hover');
										}
									break;
									case 13: // enter
										$(menu).find('LI.hover A').trigger('click');
									break;
									case 27: // esc
										$(document).trigger('click');
									break
								}
							});
							
							// When items are selected
							$('#' + o.menu).find('A').unbind('click');
							$('#' + o.menu).find('LI:not(.disabled) A').click( function() {
								$(document).unbind('click').unbind('keypress');
								$(".contextMenu").hide();
								// Callback
								if( callback ) callback( $(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y} );
								return false;
							});
							
							// Hide bindings
							setTimeout( function() { // Delay for Mozilla
								$(document).click( function() {
									$(document).unbind('click').unbind('keypress');
									$(menu).fadeOut(o.outSpeed);
									return false;
								});
							}, 0);
						

				});
				
				// Disable text selection
				if( $.browser.mozilla ) {
					$('#' + o.menu).each( function() { $(this).css({ 'MozUserSelect' : 'none' }); });
				} else if( $.browser.msie ) {
					$('#' + o.menu).each( function() { $(this).bind('selectstart.disableTextSelect', function() { return false; }); });
				} else {
					$('#' + o.menu).each(function() { $(this).bind('mousedown.disableTextSelect', function() { return false; }); });
				}
				// Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome)
				$(el).add('UL.contextMenu').bind('contextmenu', function() { return false; });
				
			});
			return $(this);
		},
		
		// Disable context menu items on the fly
		disableContextMenuItems: function(o) {
			if( o == undefined ) {
				// Disable all
				$(this).find('LI').addClass('disabled');
				return( $(this) );
			}
			$(this).each( function() {
				if( o != undefined ) {
					var d = o.split(',');
					for( var i = 0; i < d.length; i++ ) {
						$(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled');
						
					}
				}
			});
			return( $(this) );
		},
		
		// Enable context menu items on the fly
		enableContextMenuItems: function(o) {
			if( o == undefined ) {
				// Enable all
				$(this).find('LI.disabled').removeClass('disabled');
				return( $(this) );
			}
			$(this).each( function() {
				if( o != undefined ) {
					var d = o.split(',');
					for( var i = 0; i < d.length; i++ ) {
						$(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled');
						
					}
				}
			});
			return( $(this) );
		},
		
		// Disable context menu(s)
		disableContextMenu: function() {
			$(this).each( function() {
				$(this).addClass('disabled');
			});
			return( $(this) );
		},
		
		// Enable context menu(s)
		enableContextMenu: function() {
			$(this).each( function() {
				$(this).removeClass('disabled');
			});
			return( $(this) );
		},
		
		// Destroy context menu(s)
		destroyContextMenu: function() {
			// Destroy specified context menus
			$(this).each( function() {
				// Disable action
				$(this).unbind('mousedown').unbind('mouseup');
			});
			return( $(this) );
		}
		
	});
})(jQuery);
;/*!
 * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010
 * http://benalman.com/projects/jquery-bbq-plugin/
 *
 * Copyright (c) 2010 "Cowboy" Ben Alman
 * Dual licensed under the MIT and GPL licenses.
 * http://benalman.com/about/license/
 */

// Script: jQuery BBQ: Back Button & Query Library
//
// *Version: 1.2.1, Last updated: 2/17/2010*
//
// Project Home - http://benalman.com/projects/jquery-bbq-plugin/
// GitHub       - http://github.com/cowboy/jquery-bbq/
// Source       - http://github.com/cowboy/jquery-bbq/raw/master/jquery.ba-bbq.js
// (Minified)   - http://github.com/cowboy/jquery-bbq/raw/master/jquery.ba-bbq.min.js (4.0kb)
//
// About: License
//
// Copyright (c) 2010 "Cowboy" Ben Alman,
// Dual licensed under the MIT and GPL licenses.
// http://benalman.com/about/license/
//
// About: Examples
//
// These working examples, complete with fully commented code, illustrate a few
// ways in which this plugin can be used.
//
// Basic AJAX     - http://benalman.com/code/projects/jquery-bbq/examples/fragment-basic/
// Advanced AJAX  - http://benalman.com/code/projects/jquery-bbq/examples/fragment-advanced/
// jQuery UI Tabs - http://benalman.com/code/projects/jquery-bbq/examples/fragment-jquery-ui-tabs/
// Deparam        - http://benalman.com/code/projects/jquery-bbq/examples/deparam/
//
// About: Support and Testing
//
// Information about what version or versions of jQuery this plugin has been
// tested with, what browsers it has been tested in, and where the unit tests
// reside (so you can test it yourself).
//
// jQuery Versions - 1.3.2, 1.4.1, 1.4.2
// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.7, Safari 3-4,
//                   Chrome 4-5, Opera 9.6-10.1.
// Unit Tests      - http://benalman.com/code/projects/jquery-bbq/unit/
//
// About: Release History
//
// 1.2.1 - (2/17/2010) Actually fixed the stale window.location Safari bug from
//         <jQuery hashchange event> in BBQ, which was the main reason for the
//         previous release!
// 1.2   - (2/16/2010) Integrated <jQuery hashchange event> v1.2, which fixes a
//         Safari bug, the event can now be bound before DOM ready, and IE6/7
//         page should no longer scroll when the event is first bound. Also
//         added the <jQuery.param.fragment.noEscape> method, and reworked the
//         <hashchange event (BBQ)> internal "add" method to be compatible with
//         changes made to the jQuery 1.4.2 special events API.
// 1.1.1 - (1/22/2010) Integrated <jQuery hashchange event> v1.1, which fixes an
//         obscure IE8 EmulateIE7 meta tag compatibility mode bug.
// 1.1   - (1/9/2010) Broke out the jQuery BBQ event.special <hashchange event>
//         functionality into a separate plugin for users who want just the
//         basic event & back button support, without all the extra awesomeness
//         that BBQ provides. This plugin will be included as part of jQuery BBQ,
//         but also be available separately. See <jQuery hashchange event>
//         plugin for more information. Also added the <jQuery.bbq.removeState>
//         method and added additional <jQuery.deparam> examples.
// 1.0.3 - (12/2/2009) Fixed an issue in IE 6 where location.search and
//         location.hash would report incorrectly if the hash contained the ?
//         character. Also <jQuery.param.querystring> and <jQuery.param.fragment>
//         will no longer parse params out of a URL that doesn't contain ? or #,
//         respectively.
// 1.0.2 - (10/10/2009) Fixed an issue in IE 6/7 where the hidden IFRAME caused
//         a "This page contains both secure and nonsecure items." warning when
//         used on an https:// page.
// 1.0.1 - (10/7/2009) Fixed an issue in IE 8. Since both "IE7" and "IE8
//         Compatibility View" modes erroneously report that the browser
//         supports the native window.onhashchange event, a slightly more
//         robust test needed to be added.
// 1.0   - (10/2/2009) Initial release

(function($,window){
  '$:nomunge'; // Used by YUI compressor.

  // Some convenient shortcuts.
  var undefined,
    aps = Array.prototype.slice,
    decode = decodeURIComponent,

    // Method / object references.
    jq_param = $.param,
    jq_param_fragment,
    jq_deparam,
    jq_deparam_fragment,
    jq_bbq = $.bbq = $.bbq || {},
    jq_bbq_pushState,
    jq_bbq_getState,
    jq_elemUrlAttr,
    jq_event_special = $.event.special,

    // Reused strings.
    str_hashchange = 'hashchange',
    str_querystring = 'querystring',
    str_fragment = 'fragment',
    str_elemUrlAttr = 'elemUrlAttr',
    str_location = 'location',
    str_href = 'href',
    str_src = 'src',

    // Reused RegExp.
    re_trim_querystring = /^.*\?|#.*$/g,
    re_trim_fragment = /^.*\#/,
    re_no_escape,

    // Used by jQuery.elemUrlAttr.
    elemUrlAttr_cache = {};

  // A few commonly used bits, broken out to help reduce minified file size.

  function is_string( arg ) {
    return typeof arg === 'string';
  };

  // Why write the same function twice? Let's curry! Mmmm, curry..

  function curry( func ) {
    var args = aps.call( arguments, 1 );

    return function() {
      return func.apply( this, args.concat( aps.call( arguments ) ) );
    };
  };

  // Get location.hash (or what you'd expect location.hash to be) sans any
  // leading #. Thanks for making this necessary, Firefox!
  function get_fragment( url ) {
    return url.replace( /^[^#]*#?(.*)$/, '$1' );
  };

  // Get location.search (or what you'd expect location.search to be) sans any
  // leading #. Thanks for making this necessary, IE6!
  function get_querystring( url ) {
    return url.replace( /(?:^[^?#]*\?([^#]*).*$)?.*/, '$1' );
  };

  // Section: Param (to string)
  //
  // Method: jQuery.param.querystring
  //
  // Retrieve the query string from a URL or if no arguments are passed, the
  // current window.location.
  //
  // Usage:
  //
  // > jQuery.param.querystring( [ url ] );
  //
  // Arguments:
  //
  //  url - (String) A URL containing query string params to be parsed. If url
  //    is not passed, the current window.location is used.
  //
  // Returns:
  //
  //  (String) The parsed query string, with any leading "?" removed.
  //

  // Method: jQuery.param.querystring (build url)
  //
  // Merge a URL, with or without pre-existing query string params, plus any
  // object, params string or URL containing query string params into a new URL.
  //
  // Usage:
  //
  // > jQuery.param.querystring( url, params [, merge_mode ] );
  //
  // Arguments:
  //
  //  url - (String) A valid URL for params to be merged into. This URL may
  //    contain a query string and/or fragment (hash).
  //  params - (String) A params string or URL containing query string params to
  //    be merged into url.
  //  params - (Object) A params object to be merged into url.
  //  merge_mode - (Number) Merge behavior defaults to 0 if merge_mode is not
  //    specified, and is as-follows:
  //
  //    * 0: params in the params argument will override any query string
  //         params in url.
  //    * 1: any query string params in url will override params in the params
  //         argument.
  //    * 2: params argument will completely replace any query string in url.
  //
  // Returns:
  //
  //  (String) Either a params string with urlencoded data or a URL with a
  //    urlencoded query string in the format 'a=b&c=d&e=f'.

  // Method: jQuery.param.fragment
  //
  // Retrieve the fragment (hash) from a URL or if no arguments are passed, the
  // current window.location.
  //
  // Usage:
  //
  // > jQuery.param.fragment( [ url ] );
  //
  // Arguments:
  //
  //  url - (String) A URL containing fragment (hash) params to be parsed. If
  //    url is not passed, the current window.location is used.
  //
  // Returns:
  //
  //  (String) The parsed fragment (hash) string, with any leading "#" removed.

  // Method: jQuery.param.fragment (build url)
  //
  // Merge a URL, with or without pre-existing fragment (hash) params, plus any
  // object, params string or URL containing fragment (hash) params into a new
  // URL.
  //
  // Usage:
  //
  // > jQuery.param.fragment( url, params [, merge_mode ] );
  //
  // Arguments:
  //
  //  url - (String) A valid URL for params to be merged into. This URL may
  //    contain a query string and/or fragment (hash).
  //  params - (String) A params string or URL containing fragment (hash) params
  //    to be merged into url.
  //  params - (Object) A params object to be merged into url.
  //  merge_mode - (Number) Merge behavior defaults to 0 if merge_mode is not
  //    specified, and is as-follows:
  //
  //    * 0: params in the params argument will override any fragment (hash)
  //         params in url.
  //    * 1: any fragment (hash) params in url will override params in the
  //         params argument.
  //    * 2: params argument will completely replace any query string in url.
  //
  // Returns:
  //
  //  (String) Either a params string with urlencoded data or a URL with a
  //    urlencoded fragment (hash) in the format 'a=b&c=d&e=f'.

  function jq_param_sub( is_fragment, get_func, url, params, merge_mode ) {
    var result,
      qs,
      matches,
      url_params,
      hash;

    if ( params !== undefined ) {
      // Build URL by merging params into url string.

      // matches[1] = url part that precedes params, not including trailing ?/#
      // matches[2] = params, not including leading ?/#
      // matches[3] = if in 'querystring' mode, hash including leading #, otherwise ''
      matches = url.match( is_fragment ? /^([^#]*)\#?(.*)$/ : /^([^#?]*)\??([^#]*)(#?.*)/ );

      // Get the hash if in 'querystring' mode, and it exists.
      hash = matches[3] || '';

      if ( merge_mode === 2 && is_string( params ) ) {
        // If merge_mode is 2 and params is a string, merge the fragment / query
        // string into the URL wholesale, without converting it into an object.
        qs = params.replace( is_fragment ? re_trim_fragment : re_trim_querystring, '' );

      } else {
        // Convert relevant params in url to object.
        url_params = jq_deparam( matches[2] );

        params = is_string( params )

          // Convert passed params string into object.
          ? jq_deparam[ is_fragment ? str_fragment : str_querystring ]( params )

          // Passed params object.
          : params;

        qs = merge_mode === 2 ? params                              // passed params replace url params
          : merge_mode === 1  ? $.extend( {}, params, url_params )  // url params override passed params
          : $.extend( {}, url_params, params );                     // passed params override url params

        // Convert params object to a string.
        qs = jq_param( qs );

        // Unescape characters specified via $.param.noEscape. Since only hash-
        // history users have requested this feature, it's only enabled for
        // fragment-related params strings.
        if ( is_fragment ) {
          qs = qs.replace( re_no_escape, decode );
        }
      }

      // Build URL from the base url, querystring and hash. In 'querystring'
      // mode, ? is only added if a query string exists. In 'fragment' mode, #
      // is always added.
      result = matches[1] + ( is_fragment ? '#' : qs || !matches[1] ? '?' : '' ) + qs + hash;

    } else {
      // If URL was passed in, parse params from URL string, otherwise parse
      // params from window.location.
      result = get_func( url !== undefined ? url : window[ str_location ][ str_href ] );
    }

    return result;
  };

  jq_param[ str_querystring ]                  = curry( jq_param_sub, 0, get_querystring );
  jq_param[ str_fragment ] = jq_param_fragment = curry( jq_param_sub, 1, get_fragment );

  // Method: jQuery.param.fragment.noEscape
  //
  // Specify characters that will be left unescaped when fragments are created
  // or merged using <jQuery.param.fragment>, or when the fragment is modified
  // using <jQuery.bbq.pushState>. This option only applies to serialized data
  // object fragments, and not set-as-string fragments. Does not affect the
  // query string. Defaults to ",/" (comma, forward slash).
  //
  // Note that this is considered a purely aesthetic option, and will help to
  // create URLs that "look pretty" in the address bar or bookmarks, without
  // affecting functionality in any way. That being said, be careful to not
  // unescape characters that are used as delimiters or serve a special
  // purpose, such as the "#?&=+" (octothorpe, question mark, ampersand,
  // equals, plus) characters.
  //
  // Usage:
  //
  // > jQuery.param.fragment.noEscape( [ chars ] );
  //
  // Arguments:
  //
  //  chars - (String) The characters to not escape in the fragment. If
  //    unspecified, defaults to empty string (escape all characters).
  //
  // Returns:
  //
  //  Nothing.

  jq_param_fragment.noEscape = function( chars ) {
    chars = chars || '';
    var arr = $.map( chars.split(''), encodeURIComponent );
    re_no_escape = new RegExp( arr.join('|'), 'g' );
  };

  // A sensible default. These are the characters people seem to complain about
  // "uglifying up the URL" the most.
  jq_param_fragment.noEscape( ',/' );

  // Section: Deparam (from string)
  //
  // Method: jQuery.deparam
  //
  // Deserialize a params string into an object, optionally coercing numbers,
  // booleans, null and undefined values; this method is the counterpart to the
  // internal jQuery.param method.
  //
  // Usage:
  //
  // > jQuery.deparam( params [, coerce ] );
  //
  // Arguments:
  //
  //  params - (String) A params string to be parsed.
  //  coerce - (Boolean) If true, coerces any numbers or true, false, null, and
  //    undefined to their actual value. Defaults to false if omitted.
  //
  // Returns:
  //
  //  (Object) An object representing the deserialized params string.

  $.deparam = jq_deparam = function( params, coerce ) {
    var obj = {},
      coerce_types = { 'true': !0, 'false': !1, 'null': null };

    // Iterate over all name=value pairs.
    $.each( params.replace( /\+/g, ' ' ).split( '&' ), function(j,v){
      var param = v.split( '=' ),
        key = decode( param[0] ),
        val,
        cur = obj,
        i = 0,

        // If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it
        // into its component parts.
        keys = key.split( '][' ),
        keys_last = keys.length - 1;

      // If the first keys part contains [ and the last ends with ], then []
      // are correctly balanced.
      if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) {
        // Remove the trailing ] from the last keys part.
        keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );

        // Split first keys part into two parts on the [ and add them back onto
        // the beginning of the keys array.
        keys = keys.shift().split('[').concat( keys );

        keys_last = keys.length - 1;
      } else {
        // Basic 'foo' style key.
        keys_last = 0;
      }

      // Are we dealing with a name=value pair, or just a name?
      if ( param.length === 2 ) {
        val = decode( param[1] );

        // Coerce values.
        if ( coerce ) {
          val = val && !isNaN(val)            ? +val              // number
            : val === 'undefined'             ? undefined         // undefined
            : coerce_types[val] !== undefined ? coerce_types[val] // true, false, null
            : val;                                                // string
        }

        if ( keys_last ) {
          // Complex key, build deep object structure based on a few rules:
          // * The 'cur' pointer starts at the object top-level.
          // * [] = array push (n is set to array length), [n] = array if n is
          //   numeric, otherwise object.
          // * If at the last keys part, set the value.
          // * For each keys part, if the current level is undefined create an
          //   object or array based on the type of the next keys part.
          // * Move the 'cur' pointer to the next level.
          // * Rinse & repeat.
          for ( ; i <= keys_last; i++ ) {
            key = keys[i] === '' ? cur.length : keys[i];
            cur = cur[key] = i < keys_last
              ? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
              : val;
          }

        } else {
          // Simple key, even simpler rules, since only scalars and shallow
          // arrays are allowed.

          if ( $.isArray( obj[key] ) ) {
            // val is already an array, so push on the next value.
            obj[key].push( val );

          } else if ( obj[key] !== undefined ) {
            // val isn't an array, but since a second value has been specified,
            // convert val into an array.
            obj[key] = [ obj[key], val ];

          } else {
            // val is a scalar.
            obj[key] = val;
          }
        }

      } else if ( key ) {
        // No value was defined, so set something meaningful.
        obj[key] = coerce
          ? undefined
          : '';
      }
    });

    return obj;
  };

  // Method: jQuery.deparam.querystring
  //
  // Parse the query string from a URL or the current window.location,
  // deserializing it into an object, optionally coercing numbers, booleans,
  // null and undefined values.
  //
  // Usage:
  //
  // > jQuery.deparam.querystring( [ url ] [, coerce ] );
  //
  // Arguments:
  //
  //  url - (String) An optional params string or URL containing query string
  //    params to be parsed. If url is omitted, the current window.location
  //    is used.
  //  coerce - (Boolean) If true, coerces any numbers or true, false, null, and
  //    undefined to their actual value. Defaults to false if omitted.
  //
  // Returns:
  //
  //  (Object) An object representing the deserialized params string.

  // Method: jQuery.deparam.fragment
  //
  // Parse the fragment (hash) from a URL or the current window.location,
  // deserializing it into an object, optionally coercing numbers, booleans,
  // null and undefined values.
  //
  // Usage:
  //
  // > jQuery.deparam.fragment( [ url ] [, coerce ] );
  //
  // Arguments:
  //
  //  url - (String) An optional params string or URL containing fragment (hash)
  //    params to be parsed. If url is omitted, the current window.location
  //    is used.
  //  coerce - (Boolean) If true, coerces any numbers or true, false, null, and
  //    undefined to their actual value. Defaults to false if omitted.
  //
  // Returns:
  //
  //  (Object) An object representing the deserialized params string.

  function jq_deparam_sub( is_fragment, url_or_params, coerce ) {
    if ( url_or_params === undefined || typeof url_or_params === 'boolean' ) {
      // url_or_params not specified.
      coerce = url_or_params;
      url_or_params = jq_param[ is_fragment ? str_fragment : str_querystring ]();
    } else {
      url_or_params = is_string( url_or_params )
        ? url_or_params.replace( is_fragment ? re_trim_fragment : re_trim_querystring, '' )
        : url_or_params;
    }

    return jq_deparam( url_or_params, coerce );
  };

  jq_deparam[ str_querystring ]                    = curry( jq_deparam_sub, 0 );
  jq_deparam[ str_fragment ] = jq_deparam_fragment = curry( jq_deparam_sub, 1 );

  // Section: Element manipulation
  //
  // Method: jQuery.elemUrlAttr
  //
  // Get the internal "Default URL attribute per tag" list, or augment the list
  // with additional tag-attribute pairs, in case the defaults are insufficient.
  //
  // In the <jQuery.fn.querystring> and <jQuery.fn.fragment> methods, this list
  // is used to determine which attribute contains the URL to be modified, if
  // an "attr" param is not specified.
  //
  // Default Tag-Attribute List:
  //
  //  a      - href
  //  base   - href
  //  iframe - src
  //  img    - src
  //  input  - src
  //  form   - action
  //  link   - href
  //  script - src
  //
  // Usage:
  //
  // > jQuery.elemUrlAttr( [ tag_attr ] );
  //
  // Arguments:
  //
  //  tag_attr - (Object) An object containing a list of tag names and their
  //    associated default attribute names in the format { tag: 'attr', ... } to
  //    be merged into the internal tag-attribute list.
  //
  // Returns:
  //
  //  (Object) An object containing all stored tag-attribute values.

  // Only define function and set defaults if function doesn't already exist, as
  // the urlInternal plugin will provide this method as well.
  $[ str_elemUrlAttr ] || ($[ str_elemUrlAttr ] = function( obj ) {
    return $.extend( elemUrlAttr_cache, obj );
  })({
    a: str_href,
    base: str_href,
    iframe: str_src,
    img: str_src,
    input: str_src,
    form: 'action',
    link: str_href,
    script: str_src
  });

  jq_elemUrlAttr = $[ str_elemUrlAttr ];

  // Method: jQuery.fn.querystring
  //
  // Update URL attribute in one or more elements, merging the current URL (with
  // or without pre-existing query string params) plus any params object or
  // string into a new URL, which is then set into that attribute. Like
  // <jQuery.param.querystring (build url)>, but for all elements in a jQuery
  // collection.
  //
  // Usage:
  //
  // > jQuery('selector').querystring( [ attr, ] params [, merge_mode ] );
  //
  // Arguments:
  //
  //  attr - (String) Optional name of an attribute that will contain a URL to
  //    merge params or url into. See <jQuery.elemUrlAttr> for a list of default
  //    attributes.
  //  params - (Object) A params object to be merged into the URL attribute.
  //  params - (String) A URL containing query string params, or params string
  //    to be merged into the URL attribute.
  //  merge_mode - (Number) Merge behavior defaults to 0 if merge_mode is not
  //    specified, and is as-follows:
  //
  //    * 0: params in the params argument will override any params in attr URL.
  //    * 1: any params in attr URL will override params in the params argument.
  //    * 2: params argument will completely replace any query string in attr
  //         URL.
  //
  // Returns:
  //
  //  (jQuery) The initial jQuery collection of elements, but with modified URL
  //  attribute values.

  // Method: jQuery.fn.fragment
  //
  // Update URL attribute in one or more elements, merging the current URL (with
  // or without pre-existing fragment/hash params) plus any params object or
  // string into a new URL, which is then set into that attribute. Like
  // <jQuery.param.fragment (build url)>, but for all elements in a jQuery
  // collection.
  //
  // Usage:
  //
  // > jQuery('selector').fragment( [ attr, ] params [, merge_mode ] );
  //
  // Arguments:
  //
  //  attr - (String) Optional name of an attribute that will contain a URL to
  //    merge params into. See <jQuery.elemUrlAttr> for a list of default
  //    attributes.
  //  params - (Object) A params object to be merged into the URL attribute.
  //  params - (String) A URL containing fragment (hash) params, or params
  //    string to be merged into the URL attribute.
  //  merge_mode - (Number) Merge behavior defaults to 0 if merge_mode is not
  //    specified, and is as-follows:
  //
  //    * 0: params in the params argument will override any params in attr URL.
  //    * 1: any params in attr URL will override params in the params argument.
  //    * 2: params argument will completely replace any fragment (hash) in attr
  //         URL.
  //
  // Returns:
  //
  //  (jQuery) The initial jQuery collection of elements, but with modified URL
  //  attribute values.

  function jq_fn_sub( mode, force_attr, params, merge_mode ) {
    if ( !is_string( params ) && typeof params !== 'object' ) {
      // force_attr not specified.
      merge_mode = params;
      params = force_attr;
      force_attr = undefined;
    }

    return this.each(function(){
      var that = $(this),

        // Get attribute specified, or default specified via $.elemUrlAttr.
        attr = force_attr || jq_elemUrlAttr()[ ( this.nodeName || '' ).toLowerCase() ] || '',

        // Get URL value.
        url = attr && that.attr( attr ) || '';

      // Update attribute with new URL.
      that.attr( attr, jq_param[ mode ]( url, params, merge_mode ) );
    });

  };

  $.fn[ str_querystring ] = curry( jq_fn_sub, str_querystring );
  $.fn[ str_fragment ]    = curry( jq_fn_sub, str_fragment );

  // Section: History, hashchange event
  //
  // Method: jQuery.bbq.pushState
  //
  // Adds a 'state' into the browser history at the current position, setting
  // location.hash and triggering any bound <hashchange event> callbacks
  // (provided the new state is different than the previous state).
  //
  // If no arguments are passed, an empty state is created, which is just a
  // shortcut for jQuery.bbq.pushState( {}, 2 ).
  //
  // Usage:
  //
  // > jQuery.bbq.pushState( [ params [, merge_mode ] ] );
  //
  // Arguments:
  //
  //  params - (String) A serialized params string or a hash string beginning
  //    with # to merge into location.hash.
  //  params - (Object) A params object to merge into location.hash.
  //  merge_mode - (Number) Merge behavior defaults to 0 if merge_mode is not
  //    specified (unless a hash string beginning with # is specified, in which
  //    case merge behavior defaults to 2), and is as-follows:
  //
  //    * 0: params in the params argument will override any params in the
  //         current state.
  //    * 1: any params in the current state will override params in the params
  //         argument.
  //    * 2: params argument will completely replace current state.
  //
  // Returns:
  //
  //  Nothing.
  //
  // Additional Notes:
  //
  //  * Setting an empty state may cause the browser to scroll.
  //  * Unlike the fragment and querystring methods, if a hash string beginning
  //    with # is specified as the params agrument, merge_mode defaults to 2.

  jq_bbq.pushState = jq_bbq_pushState = function( params, merge_mode ) {
    if ( is_string( params ) && /^#/.test( params ) && merge_mode === undefined ) {
      // Params string begins with # and merge_mode not specified, so completely
      // overwrite window.location.hash.
      merge_mode = 2;
    }

    var has_args = params !== undefined,
      // Merge params into window.location using $.param.fragment.
      url = jq_param_fragment( window[ str_location ][ str_href ],
        has_args ? params : {}, has_args ? merge_mode : 2 );

    // Set new window.location.href. If hash is empty, use just # to prevent
    // browser from reloading the page. Note that Safari 3 & Chrome barf on
    // location.hash = '#'.
    window[ str_location ][ str_href ] = url + ( /#/.test( url ) ? '' : '#' );
  };

  // Method: jQuery.bbq.getState
  //
  // Retrieves the current 'state' from the browser history, parsing
  // location.hash for a specific key or returning an object containing the
  // entire state, optionally coercing numbers, booleans, null and undefined
  // values.
  //
  // Usage:
  //
  // > jQuery.bbq.getState( [ key ] [, coerce ] );
  //
  // Arguments:
  //
  //  key - (String) An optional state key for which to return a value.
  //  coerce - (Boolean) If true, coerces any numbers or true, false, null, and
  //    undefined to their actual value. Defaults to false.
  //
  // Returns:
  //
  //  (Anything) If key is passed, returns the value corresponding with that key
  //    in the location.hash 'state', or undefined. If not, an object
  //    representing the entire 'state' is returned.

  jq_bbq.getState = jq_bbq_getState = function( key, coerce ) {
    return key === undefined || typeof key === 'boolean'
      ? jq_deparam_fragment( key ) // 'key' really means 'coerce' here
      : jq_deparam_fragment( coerce )[ key ];
  };

  // Method: jQuery.bbq.removeState
  //
  // Remove one or more keys from the current browser history 'state', creating
  // a new state, setting location.hash and triggering any bound
  // <hashchange event> callbacks (provided the new state is different than
  // the previous state).
  //
  // If no arguments are passed, an empty state is created, which is just a
  // shortcut for jQuery.bbq.pushState( {}, 2 ).
  //
  // Usage:
  //
  // > jQuery.bbq.removeState( [ key [, key ... ] ] );
  //
  // Arguments:
  //
  //  key - (String) One or more key values to remove from the current state,
  //    passed as individual arguments.
  //  key - (Array) A single array argument that contains a list of key values
  //    to remove from the current state.
  //
  // Returns:
  //
  //  Nothing.
  //
  // Additional Notes:
  //
  //  * Setting an empty state may cause the browser to scroll.

  jq_bbq.removeState = function( arr ) {
    var state = {};

    // If one or more arguments is passed..
    if ( arr !== undefined ) {

      // Get the current state.
      state = jq_bbq_getState();

      // For each passed key, delete the corresponding property from the current
      // state.
      $.each( $.isArray( arr ) ? arr : arguments, function(i,v){
        delete state[ v ];
      });
    }

    // Set the state, completely overriding any existing state.
    jq_bbq_pushState( state, 2 );
  };

  // Event: hashchange event (BBQ)
  //
  // Usage in jQuery 1.4 and newer:
  //
  // In jQuery 1.4 and newer, the event object passed into any hashchange event
  // callback is augmented with a copy of the location.hash fragment at the time
  // the event was triggered as its event.fragment property. In addition, the
  // event.getState method operates on this property (instead of location.hash)
  // which allows this fragment-as-a-state to be referenced later, even after
  // window.location may have changed.
  //
  // Note that event.fragment and event.getState are not defined according to
  // W3C (or any other) specification, but will still be available whether or
  // not the hashchange event exists natively in the browser, because of the
  // utility they provide.
  //
  // The event.fragment property contains the output of <jQuery.param.fragment>
  // and the event.getState method is equivalent to the <jQuery.bbq.getState>
  // method.
  //
  // > $(window).bind( 'hashchange', function( event ) {
  // >   var hash_str = event.fragment,
  // >     param_obj = event.getState(),
  // >     param_val = event.getState( 'param_name' ),
  // >     param_val_coerced = event.getState( 'param_name', true );
  // >   ...
  // > });
  //
  // Usage in jQuery 1.3.2:
  //
  // In jQuery 1.3.2, the event object cannot to be augmented as in jQuery 1.4+,
  // so the fragment state isn't bound to the event object and must instead be
  // parsed using the <jQuery.param.fragment> and <jQuery.bbq.getState> methods.
  //
  // > $(window).bind( 'hashchange', function( event ) {
  // >   var hash_str = $.param.fragment(),
  // >     param_obj = $.bbq.getState(),
  // >     param_val = $.bbq.getState( 'param_name' ),
  // >     param_val_coerced = $.bbq.getState( 'param_name', true );
  // >   ...
  // > });
  //
  // Additional Notes:
  //
  // * Due to changes in the special events API, jQuery BBQ v1.2 or newer is
  //   required to enable the augmented event object in jQuery 1.4.2 and newer.
  // * See <jQuery hashchange event> for more detailed information.

  jq_event_special[ str_hashchange ] = $.extend( jq_event_special[ str_hashchange ], {

    // Augmenting the event object with the .fragment property and .getState
    // method requires jQuery 1.4 or newer. Note: with 1.3.2, everything will
    // work, but the event won't be augmented)
    add: function( handleObj ) {
      var old_handler;

      function new_handler(e) {
        // e.fragment is set to the value of location.hash (with any leading #
        // removed) at the time the event is triggered.
        var hash = e[ str_fragment ] = jq_param_fragment();

        // e.getState() works just like $.bbq.getState(), but uses the
        // e.fragment property stored on the event object.
        e.getState = function( key, coerce ) {
          return key === undefined || typeof key === 'boolean'
            ? jq_deparam( hash, key ) // 'key' really means 'coerce' here
            : jq_deparam( hash, coerce )[ key ];
        };

        old_handler.apply( this, arguments );
      };

      // This may seem a little complicated, but it normalizes the special event
      // .add method between jQuery 1.4/1.4.1 and 1.4.2+
      if ( $.isFunction( handleObj ) ) {
        // 1.4, 1.4.1
        old_handler = handleObj;
        return new_handler;
      } else {
        // 1.4.2+
        old_handler = handleObj.handler;
        handleObj.handler = new_handler;
      }
    }

  });

})(jQuery,this);

/*!
 * jQuery hashchange event - v1.2 - 2/11/2010
 * http://benalman.com/projects/jquery-hashchange-plugin/
 *
 * Copyright (c) 2010 "Cowboy" Ben Alman
 * Dual licensed under the MIT and GPL licenses.
 * http://benalman.com/about/license/
 */

// Script: jQuery hashchange event
//
// *Version: 1.2, Last updated: 2/11/2010*
//
// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
// GitHub       - http://github.com/cowboy/jquery-hashchange/
// Source       - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
// (Minified)   - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (1.1kb)
//
// About: License
//
// Copyright (c) 2010 "Cowboy" Ben Alman,
// Dual licensed under the MIT and GPL licenses.
// http://benalman.com/about/license/
//
// About: Examples
//
// This working example, complete with fully commented code, illustrate one way
// in which this plugin can be used.
//
// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
//
// About: Support and Testing
//
// Information about what version or versions of jQuery this plugin has been
// tested with, what browsers it has been tested in, and where the unit tests
// reside (so you can test it yourself).
//
// jQuery Versions - 1.3.2, 1.4.1, 1.4.2
// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.7, Safari 3-4, Chrome, Opera 9.6-10.1.
// Unit Tests      - http://benalman.com/code/projects/jquery-hashchange/unit/
//
// About: Known issues
//
// While this jQuery hashchange event implementation is quite stable and robust,
// there are a few unfortunate browser bugs surrounding expected hashchange
// event-based behaviors, independent of any JavaScript window.onhashchange
// abstraction. See the following examples for more information:
//
// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
//
// About: Release History
//
// 1.2   - (2/11/2010) Fixed a bug where coming back to a page using this plugin
//         from a page on another domain would cause an error in Safari 4. Also,
//         IE6/7 Iframe is now inserted after the body (this actually works),
//         which prevents the page from scrolling when the event is first bound.
//         Event can also now be bound before DOM ready, but it won't be usable
//         before then in IE6/7.
// 1.1   - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
//         where browser version is incorrectly reported as 8.0, despite
//         inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
// 1.0   - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
//         window.onhashchange functionality into a separate plugin for users
//         who want just the basic event & back button support, without all the
//         extra awesomeness that BBQ provides. This plugin will be included as
//         part of jQuery BBQ, but also be available separately.

(function($,window,undefined){
  '$:nomunge'; // Used by YUI compressor.

  // Method / object references.
  var fake_onhashchange,
    jq_event_special = $.event.special,

    // Reused strings.
    str_location = 'location',
    str_hashchange = 'hashchange',
    str_href = 'href',

    // IE6/7 specifically need some special love when it comes to back-button
    // support, so let's do a little browser sniffing..
    browser = $.browser,
    mode = document.documentMode,
    is_old_ie = browser.msie && ( mode === undefined || mode < 8 ),

    // Does the browser support window.onhashchange? Test for IE version, since
    // IE8 incorrectly reports this when in "IE7" or "IE8 Compatibility View"!
    supports_onhashchange = 'on' + str_hashchange in window && !is_old_ie;

  // Get location.hash (or what you'd expect location.hash to be) sans any
  // leading #. Thanks for making this necessary, Firefox!
  function get_fragment( url ) {
    url = url || window[ str_location ][ str_href ];
    return url.replace( /^[^#]*#?(.*)$/, '$1' );
  };

  // Property: jQuery.hashchangeDelay
  //
  // The numeric interval (in milliseconds) at which the <hashchange event>
  // polling loop executes. Defaults to 100.

  $[ str_hashchange + 'Delay' ] = 100;

  // Event: hashchange event
  //
  // Fired when location.hash changes. In browsers that support it, the native
  // window.onhashchange event is used (IE8, FF3.6), otherwise a polling loop is
  // initialized, running every <jQuery.hashchangeDelay> milliseconds to see if
  // the hash has changed. In IE 6 and 7, a hidden Iframe is created to allow
  // the back button and hash-based history to work.
  //
  // Usage:
  //
  // > $(window).bind( 'hashchange', function(e) {
  // >   var hash = location.hash;
  // >   ...
  // > });
  //
  // Additional Notes:
  //
  // * The polling loop and Iframe are not created until at least one callback
  //   is actually bound to 'hashchange'.
  // * If you need the bound callback(s) to execute immediately, in cases where
  //   the page 'state' exists on page load (via bookmark or page refresh, for
  //   example) use $(window).trigger( 'hashchange' );
  // * The event can be bound before DOM ready, but since it won't be usable
  //   before then in IE6/7 (due to the necessary Iframe), recommended usage is
  //   to bind it inside a $(document).ready() callback.

  jq_event_special[ str_hashchange ] = $.extend( jq_event_special[ str_hashchange ], {

    // Called only when the first 'hashchange' event is bound to window.
    setup: function() {
      // If window.onhashchange is supported natively, there's nothing to do..
      if ( supports_onhashchange ) { return false; }

      // Otherwise, we need to create our own. And we don't want to call this
      // until the user binds to the event, just in case they never do, since it
      // will create a polling loop and possibly even a hidden Iframe.
      $( fake_onhashchange.start );
    },

    // Called only when the last 'hashchange' event is unbound from window.
    teardown: function() {
      // If window.onhashchange is supported natively, there's nothing to do..
      if ( supports_onhashchange ) { return false; }

      // Otherwise, we need to stop ours (if possible).
      $( fake_onhashchange.stop );
    }

  });

  // fake_onhashchange does all the work of triggering the window.onhashchange
  // event for browsers that don't natively support it, including creating a
  // polling loop to watch for hash changes and in IE 6/7 creating a hidden
  // Iframe to enable back and forward.
  fake_onhashchange = (function(){
    var self = {},
      timeout_id,
      iframe,
      set_history,
      get_history;

    // Initialize. In IE 6/7, creates a hidden Iframe for history handling.
    function init(){
      // Most browsers don't need special methods here..
      set_history = get_history = function(val){ return val; };

      // But IE6/7 do!
      if ( is_old_ie ) {

        // Create hidden Iframe after the end of the body to prevent initial
        // page load from scrolling unnecessarily.
        iframe = $('<iframe src="javascript:0"/>').hide().insertAfter( 'body' )[0].contentWindow;

        // Get history by looking at the hidden Iframe's location.hash.
        get_history = function() {
          return get_fragment( iframe.document[ str_location ][ str_href ] );
        };

        // Set a new history item by opening and then closing the Iframe
        // document, *then* setting its location.hash.
        set_history = function( hash, history_hash ) {
          if ( hash !== history_hash ) {
            var doc = iframe.document;
            doc.open().close();
            doc[ str_location ].hash = '#' + hash;
          }
        };

        // Set initial history.
        set_history( get_fragment() );
      }
    };

    // Start the polling loop.
    self.start = function() {
      // Polling loop is already running!
      if ( timeout_id ) { return; }

      // Remember the initial hash so it doesn't get triggered immediately.
      var last_hash = get_fragment();

      // Initialize if not yet initialized.
      set_history || init();

      // This polling loop checks every $.hashchangeDelay milliseconds to see if
      // location.hash has changed, and triggers the 'hashchange' event on
      // window when necessary.
      (function loopy(){
        var hash = get_fragment(),
          history_hash = get_history( last_hash );

        if ( hash !== last_hash ) {
          set_history( last_hash = hash, history_hash );

          $(window).trigger( str_hashchange );

        } else if ( history_hash !== last_hash ) {
          window[ str_location ][ str_href ] = window[ str_location ][ str_href ].replace( /#.*/, '' ) + '#' + history_hash;
        }

        timeout_id = setTimeout( loopy, $[ str_hashchange + 'Delay' ] );
      })();
    };

    // Stop the polling loop, but only if an IE6/7 Iframe wasn't created. In
    // that case, even if there are no longer any bound event handlers, the
    // polling loop is still necessary for back/next to work at all!
    self.stop = function() {
      if ( !iframe ) {
        timeout_id && clearTimeout( timeout_id );
        timeout_id = 0;
      }
    };

    return self;
  })();

})(jQuery,this);

;/*
 * Lazy Load - jQuery plugin for lazy loading images
 *
 * Copyright (c) 2007-2012 Mika Tuupola
 *
 * Licensed under the MIT license:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Project home:
 *   http://www.appelsiini.net/projects/lazyload
 *
 * Version:  1.7.0
 *
 */
(function($, window) {

    $window = $(window);

    $.fn.lazyload = function(options) {
        var settings = {
            threshold       : 0,
            failure_limit   : 0,
            event           : "scroll",
            effect          : "show",
            container       : window,
            data_attribute  : "original",
            skip_invisible  : true,
            appear          : null,
            load            : null
        };

        if(options) {
            /* Maintain BC for a couple of version. */
            if (undefined !== options.failurelimit) {
                options.failure_limit = options.failurelimit;
                delete options.failurelimit;
            }
            if (undefined !== options.effectspeed) {
                options.effect_speed = options.effectspeed;
                delete options.effectspeed;
            }

            $.extend(settings, options);
        }

        /* Fire one scroll event per scroll. Not one scroll event per image. */
        var elements = this;
        if (0 == settings.event.indexOf("scroll")) {
            $(settings.container).bind(settings.event, function(event) {
                var counter = 0;
                elements.each(function() {
                    $this = $(this);
                    if (settings.skip_invisible && !$this.is(":visible")) return;
                    if ($.abovethetop(this, settings) ||
                        $.leftofbegin(this, settings)) {
                            /* Nothing. */
                    } else if (!$.belowthefold(this, settings) &&
                        !$.rightoffold(this, settings)) {
                            $this.trigger("appear");
                    } else {
                        if (++counter > settings.failure_limit) {
                            return false;
                        }
                    }
                });
            });
        }

        this.each(function() {
            var self = this;
            var $self = $(self);

            self.loaded = false;

            /* When appear is triggered load original image. */
            $self.one("appear", function() {
                if (!this.loaded) {
                    if (settings.appear) {
                        var elements_left = elements.length;
                        settings.appear.call(self, elements_left, settings);
                    }
                    $("<img />")
                        .bind("load", function() {
                            $self
                                .hide()
                                .attr("src", $self.data(settings.data_attribute))
                                [settings.effect](settings.effect_speed);
                            self.loaded = true;

                            /* Remove image from array so it is not looped next time. */
                            var temp = $.grep(elements, function(element) {
                                return !element.loaded;
                            });
                            elements = $(temp);

                            if (settings.load) {
                                var elements_left = elements.length;
                                settings.load.call(self, elements_left, settings);
                            }
                        })
                        .attr("src", $self.data(settings.data_attribute));
                };
            });

            /* When wanted event is triggered load original image */
            /* by triggering appear.                              */
            if (0 != settings.event.indexOf("scroll")) {
                $self.bind(settings.event, function(event) {
                    if (!self.loaded) {
                        $self.trigger("appear");
                    }
                });
            }
        });

        var event_registered = $(settings.container).data("jquery_lazyload_registered_"+settings.event);
        if (!event_registered)
        {
            $(settings.container).data("jquery_lazyload_registered_"+settings.event, true);
            /* Check if something appears when window is resized. */
            $window.bind("resize", function(event) {
                $(settings.container).trigger(settings.event);
            });
        }

        /* Force initial check if images should appear. */
        $(settings.container).trigger(settings.event);

        return this;

    };

    /* Convenience methods in jQuery namespace.           */
    /* Use as  $.belowthefold(element, {threshold : 100, container : window}) */

    $.belowthefold = function(element, settings) {
        if (settings.container === undefined || settings.container === window) {
            var fold = $window.height() + $window.scrollTop();
        } else {
            var fold = $(settings.container).offset().top + $(settings.container).height();
        }
        return fold <= $(element).offset().top - settings.threshold;
    };

    $.rightoffold = function(element, settings) {
        if (settings.container === undefined || settings.container === window) {
            var fold = $window.width() + $window.scrollLeft();
        } else {
            var fold = $(settings.container).offset().left + $(settings.container).width();
        }
        return fold <= $(element).offset().left - settings.threshold;
    };

    $.abovethetop = function(element, settings) {
        if (settings.container === undefined || settings.container === window) {
            var fold = $window.scrollTop();
        } else {
            var fold = $(settings.container).offset().top;
        }
        return fold >= $(element).offset().top + settings.threshold  + $(element).height();
    };

    $.leftofbegin = function(element, settings) {
        if (settings.container === undefined || settings.container === window) {
            var fold = $window.scrollLeft();
        } else {
            var fold = $(settings.container).offset().left;
        }
        return fold >= $(element).offset().left + settings.threshold + $(element).width();
    };

    $.inviewport = function(element, settings) {
         return !$.rightofscreen(element, settings) && !$.leftofscreen(element, settings) &&
                !$.belowthefold(element, settings) && !$.abovethetop(element, settings);
     };

    /* Custom selectors for your convenience.   */
    /* Use as $("img:below-the-fold").something() */

    $.extend($.expr[':'], {
        "below-the-fold" : function(a) { return $.belowthefold(a, {threshold : 0, container: window}) },
        "above-the-top"  : function(a) { return !$.belowthefold(a, {threshold : 0, container: window}) },
        "right-of-screen": function(a) { return $.rightoffold(a, {threshold : 0, container: window}) },
        "left-of-screen" : function(a) { return !$.rightoffold(a, {threshold : 0, container: window}) },
        "in-viewport"    : function(a) { return !$.inviewport(a, {threshold : 0, container: window}) },
        /* Maintain BC for couple of versions. */
        "above-the-fold" : function(a) { return !$.belowthefold(a, {threshold : 0, container: window}) },
        "right-of-fold"  : function(a) { return $.rightoffold(a, {threshold : 0, container: window}) },
        "left-of-fold"   : function(a) { return !$.rightoffold(a, {threshold : 0, container: window}) }
    });

})(jQuery, window);

;(function(sdwndw)
{
    sdwndw.sdthreadrate_getting = 0;
    sdwndw.sdthreadrate_postid = 0;
    sdwndw.sdthreadrate_url = "";
    sdwndw.disableLegacyDialog = sdwndw.disableLegacyDialog || false;

    sdwndw.sdthreadrate_vote = function(postid, vote, controltype, forumid, firstPostThumbs, action_source)
    {
        var abandoned = false;
        controltype = typeof(controltype) != 'undefined' ? controltype : "thread";
        firstPostThumbs = firstPostThumbs || false;
        action_source = action_source || '';

        // use url embeded on page
        if (typeof(fb_object_url) != 'undefined')
        {
            sdthreadrate_url = fb_object_url;
        }
        else
        {
            if (controltype == "permadeal")
            {
                sdthreadrate_url = $("#sdpermadeal_" + postid).find("a.tup").attr("facebook_url");
            }
            else
            {
                sdthreadrate_url = $("#sdhottopic_" + postid).find("a.voteup").attr("facebook_url");
            }
        }
        var object_title_override = false;
        var do_fb_vote = false;
        var votefeedback;
        var prefix = '';
        if (controltype == 'modal_deal' || controltype == 'modal_coupon')
        {
            votefeedback = $('#modal_vote_feedback_' + postid + '_' + vote);
        }
        else if (controltype == 'modern')
        {
            votefeedback = $("#vote_feedback_" + forumid + '_' + vote + (firstPostThumbs ? '_first' : ''));
            votefeedback.find("input[postid]").attr("postid", postid).prop("checked", false);
        }
        else if (controltype == 'modern_modal' || controltype == 'deal_feedback_modal' || controltype == 'vote_modal')
        {
            if (controltype == 'vote_modal')
            {
                prefix = 'vote_';
            }
            votefeedback = $('#' + prefix + 'modal_vote_feedback_' + forumid + '_' + vote);
            votefeedback.find("input[postid]").attr("postid", postid).prop("checked", false);
        }
        else
        {
            votefeedback = $("#vote_feedback_" + postid + "_" + vote);
        }
        if (votefeedback.length > 0)
        {
            if (votefeedback.find("input").length == 1)
            {
                if (vote == 1 && typeof sdfacebook != "undefined" && sdfacebook.SDLoggedIn)
                {
                    if (controltype == "permadeal")
                    {
                        object_title_override = $("h1#fb_deal_title_" + postid).html();
                        do_fb_vote = true;
                        if (typeof(fb_action) == "undefined")
                        {
                            fb_action = "deal";
                        }
                    }
                    else if (controltype == "fpdeal")
                    {
                        do_fb_vote = true;
                        if (typeof(fb_action) == "undefined")
                        {
                            fb_action = "deal";
                        }
                    }
                    else if (controltype == 'hottopic')
                    {
                        object_title_override = $("#sdhottopic_" + postid).find("a.fb_popular_deal_title").html();
                        do_fb_vote = true;
                        if (typeof(fb_action) == "undefined")
                        {
                            fb_action = "deal";
                        }
                    }
                    else if (controltype == 'thread')
                    {
                        do_fb_vote = true;
                    }
                    else if (controltype == 'modal_deal' || controltype == 'modal_coupon')
                    {
                        do_fb_vote = true;
                        if (typeof(fb_action) == "undefined")
                        {
                            fb_action = "deal";
                        }
                    }

                    sdthreadrate_submit(postid, vote, votefeedback.find("input").val(), controltype, undefined, undefined, action_source);
                }
                else
                {
                    sdthreadrate_submit(postid, vote, votefeedback.find("input").val(), controltype, undefined, undefined, action_source);
                }
            }
            else
            {
                votefeedback.find(".voteselected").attr("postid", postid);
                votefeedback.find(".voteselected").attr("vote", vote);
                votefeedback.find(".voteselected").attr("action_source", action_source);
                if (controltype != 'modern')
                {
                    votefeedback.slideDown("fast");
                }
            }
        }
        else
        {
            sdthreadrate_submit(postid, vote, 0, controltype, undefined, undefined, action_source);
        }
    }

    sdwndw.sdthreadrate_extendedvote = function(input, controltype)
    {
        input = $(input);
        let vote = input.attr("vote");
        let postid = input.attr("postid");
        let forumid = input.attr("forumid");
        let action_source = input.attr("action_source");
        let votefeedback = $("#vote_feedback_" + forumid + "_" + vote);
        if ($.fn.dropdown && votefeedback.length > 0 && votefeedback.dropdown)
        {
            votefeedback.dropdown("hide");
        }
        $("#vote_feedback_" + postid + "_" + vote).slideUp("fast");
        $("#modal_vote_feedback_" + postid + "_" + vote).slideUp("fast");
        sdthreadrate_submit(postid, vote, input.val(), controltype, undefined, undefined, action_source);
    }

    sdwndw.sdthreadrate_submit = function(postid, vote, votetypeid, controltype, user_response, show_error, action_source)
    {
        controltype = typeof(controltype) != 'undefined' ? controltype : "thread";
        action_source = action_source || '';

        if (typeof show_error == "undefined")
        {
            show_error = true;
        }

        if (controltype == "merchant_widget")
        {
            show_error = false;
        }

        if (disableLegacyDialog === true)
        {
            show_error = false;
        }

        if (sdthreadrate_getting == 1)
        {
            return false;
        }
        else
        {
            sdthreadrate_getting = 1;
        }
        if (controltype == "coupon")
        {
            $("#votecontrol_wrap_" + postid).btOff();
        }
        else if (controltype == "permadeal")
        {
            $("#ratecontrol_" + postid + "_" + vote).css("background-image", "url(https://js.slickdealscdn.com/images/buttons/ajaxsmall.gif)");
        }
        else if (controltype == "merchant_widget")
        {
            $(".tup").css("background-image", "url(https://js.slickdealscdn.com/images/buttons/ajaxsmall.gif)");
        }

        var currentControlType = controltype;
        if (controltype == "mobile3" || controltype == "searchlanding")
        {
            controltype = "modern";
        }

        var sdthreadrate_param = {
            'ajax': 1,
            'do': "sdthreadratevote",
            'postid': postid,
            'vote': vote,
            'votetypeid': votetypeid,
            'controltype': controltype,
            'securitytoken': SECURITYTOKEN,
            'where_from': '/forums/sdthreadrate_ajax.php',
            'action_source': action_source,
        };

        if (typeof FACEBOOK_OFF != "undefined" && !FACEBOOK_OFF && typeof sdfacebook != "undefined")
        {
            if (sdfacebook.fb_message != "")
            {
                sdthreadrate_param['fb_message'] = sdfacebook.fb_message;
            }
        }

        var voteimgindex = vote + 1, vtarget = $("#ratecontrol_" + postid + " img:eq(" + voteimgindex + ")"), voteorigimg = vtarget.attr('src');
        vtarget.attr("src", "/images/buttons/ajaxsmall.gif");

        //ADDED David - Prompt for sharing on facebook before we vote
        if (user_response)
        {
            sdthreadrate_param['fb_vote'] = user_response.answer;
            sdthreadrate_param['fb_options'] = user_response.options;
            sdthreadrate_param['fb_object'] = fb_action;
            sdthreadrate_param['fb_url'] = sdthreadrate_url;
            sdthreadrate_param['fb_explicit'] = user_response.explicit;
        }
        // END

        $.post('/forums/sdthreadrate_ajax.php', sdthreadrate_param, function(data)
        {
            // Notify Audigent that a user attempted to submit a vote.  Note
            // that this gets fired regardless of whether the attempted vote
            // was successful or not, since they are interested in the intent
            // to vote more than the fact that a vote was successful.
            if (typeof window.au !== 'undefined' && typeof window.au.push !== 'undefined')
            {
                const auuidCookieName = 'auuid';
                const auuid = document.cookie.match('(^|;) ?' + auuidCookieName + '=([^;]*)(;|$)');
                const auPushMessage = {
                    'category': 'sendEvent',
                    'events': [
                        {
                            'type': sdthreadrate_param['vote'] > 0 ? 'upvote' : 'downvote',
                            'user_id': dataLayer.visitor.userId || null,
                            'anon_id': auuid ? auuid[2] : null,
                            'thread_id': sdthreadrate_param['postid'],
                        }
                    ]
                };

                window.au.push(auPushMessage);
            }

            sdthreadrate_getting = 0;
            controltype = currentControlType;
            if ($(data).find("error").length === 0)
            {
                $("#sharethistab:visible").hide();
                $("#showfeedback:hidden").show();
                $("#fistthread_publicopinion:hidden").css("visibility", "").hide().fadeIn();
                var postid;
                if (controltype == "modern" || controltype == "deal_feedback_modal")
                {
                    postid = $(data).find("htmlbit").attr("postid");

                    var redir = $(data).find('redirecturl');
                    if (redir.length > 0)
                    {
                        if (redir.attr('delay'))
                        {
                            setTimeout("window.location = '" + redir.text() + "';", redir.attr('delay'));
                        }
                        else
                        {
                            window.location = redir.text();
                        }
                    }
                }
                else
                {
                    postid = geturlvar(this.data, "postid");
                    updateHtmlBits(data);
                }
                var skipSlide = false;
                var score = $(".vote_score");
                score.hide();
                score.slideDown("normal");
                var error = $(data).find("errormsg");
                var success = $(data).find("successmsg");
                var custom = $(data).find("custommsg");

                if (success.length > 0)
                {
                    successdialog(success.text());
                }

                var voteError = false;

                if (custom.length > 0 && currentControlType != "mobile3")
                {
                    genericdialog(custom.attr("title"), custom.text(), false, true);
                    voteError = custom.attr("voteError");
                }

                if (error.length > 0)
                {
                    if (show_error)
                    {
                        errordialog(error.text(), null, 300);
                    }
                }
                else if ($(data).find('dologin').length == 1)
                {
                    var action_source = 'Thread Vote';

                    if (controltype == 'mobile3')
                    {
                        var urlParams = {
                            action_source: 'Mobile Web ' + action_source,
                            url: location.pathname + location.search
                        };

                        if (location.search.indexOf('exp=brx') > -1)
                        {
                            urlParams['exp'] = 'brx';
                        }

                        //We are on mobile so we need to redirect instead of calling the ajax login
                        location.href = "/forums/register.php?" + $.param(urlParams)
                    }
                    else
                    {
                        vtarget.attr('src', voteorigimg);

                        if (score.length === 0)
                        {
                            if (controltype == 'modal_deal' || controltype == 'modal_coupon')
                            {
                                sd_ajax_login({
                                    form: null,
                                    hide_social_buttons: null,
                                    custom_message: null,
                                    callback_function: null,
                                    email: null,
                                    nousertrap: null,
                                    src: 'acmodal',
                                    action_source: action_source
                                });
                            }
                            else if (controltype == 'deal_feedback_modal')
                            {
                                if (vote > 0)
                                {
                                    action_source = 'Deal Feedback Modal - Like';
                                }
                                else if (vote < 0)
                                {
                                    action_source = 'Deal Feedback Modal - Dislike';
                                }

                                sd_ajax_login({action_source: action_source});
                            }
                            else
                            {
                                sd_ajax_login({action_source: action_source});
                            }
                        }
                        else
                        {
                            score.slideDown("normal", function()
                            {
                                sd_ajax_login({action_source: action_source});
                            });
                            skipSlide = true;
                        }
                    }
                }
                else if ((controltype == "modern" || controltype == "modern_modal" || controltype == 'vote_modal') && !voteError)
                {
                    // Don't change the vote displayed if there was an error (duplicate vote or time limit exceeded)
                    // voteError flag set when custom dialog is used above, and in sdthreadrate_ajax.php
                    var htmlbit = $(data).find("htmlbit");
                    apply_threadrate_styles(htmlbit.attr("postid"), vote, htmlbit.attr("pos"), htmlbit.attr("neg"), htmlbit.attr("score"));
                    var threadrating = $("[role='threadrating'][data-postid='" + htmlbit.attr("postid") + "']"),
                        threadlabel = $(".likesContainer[data-postid='" + htmlbit.attr("postid") + "']");
                    threadrating.find("[role='thread.sign']").text(htmlbit.attr("sign"));
                    threadrating.find("[role='thread.score']").text(htmlbit.attr("score"));

                    var signClass = "default";
                    if (htmlbit.attr("sign") === "+")
                    {
                        signClass = "positive";
                        threadlabel.find(".icon-thumbsdown").removeClass("icon-thumbsdown").addClass("icon-thumbsup");
                    }
                    else if (parseInt(htmlbit.attr("score"), 10) < 0)
                    {
                        signClass = "negative";
                        threadlabel.find(".icon-thumbsup").removeClass("icon-thumbsup").addClass("icon-thumbsdown");
                    }
                    threadrating.find("[role='thread.score']").parent().addClass("voteCount").add(threadlabel)
                        .removeClass("positive").removeClass("negative").addClass(signClass);
                }
                else if (controltype == "coupon")
                {
                    $("#votecontrol_wrap_" + postid).parents(".coupon_info").removeClass("pos").removeClass("neg").addClass(vote > 0 ? "pos" : "neg");
                }
                else if (controltype == "mobile3" || controltype == "searchlanding")
                {
                    var htmlbit = $(data).find("htmlbit");
                    var postId = htmlbit.attr("postid"),
                        newScore = htmlbit.attr("score");
                    var $threadRatingContainer = $("[role='threadrating'][data-postid='" + postId + "']"),
                        $threadScore = $('[role="thread.score"][data-postid="' + postId + '"]');
                    var $voteElements = $threadRatingContainer.find('[role="button"][data-vote]');

                    $voteElements.removeClass('voted');
                    $voteElements.filter(function(idx, elm)
                    {
                        return $(elm).data('vote') === vote;
                    }).addClass('voted');

                    $threadScore.text((htmlbit.attr("sign") === '+' ? '' : htmlbit.attr("sign")) + '' + newScore);
                    $threadRatingContainer.trigger('voteCompleted', {
                        'postId': postId,
                        'vote': vote,
                        'score': newScore
                    });
                }
                else if (controltype == "permadeal")
                {
                    $(".score").hide().fadeIn();
                }
                else if (controltype == 'merchant_widget')
                {
                    $(".tup").hide().after('<div class="thanks">Thanks for buying!</div>');
                }
                else if (controltype == "fpdeal")
                {
                    $("#thumbup_" + postid).addClass("pos");
                }
                else if (controltype == 'hottopic')
                {
                    postid = $(data).find('htmlbits htmlbit').attr('id');
                    $('#sdhottopic_' + postid + ' td.htscore').html($(data).find('htmlbits htmlbit').text());
                }
                else if (controltype == 'ddpr')
                {
                    // update thread score
                    let response = data.querySelector('htmlbit');
                    let threadScore = document.querySelector('[role="thread.score"]');
                    let threadSign = document.querySelector('[role="thread.sign"]');
                    threadScore.innerText = response.getAttribute('score');
                    threadSign.innerText = response.getAttribute('sign');

                    // update thumb buttons
                    let upScore = document.querySelector('.upvote .voteCount');
                    let downScore = document.querySelector('.downvote .voteCount');
                    let existingVote = document.querySelector('.thumbsBlock .voted');
                    let newVoteType = null;
                    let btnSelected = null;
                    let newVoteValue = parseInt(response.getAttribute('voted'), 10);

                    if (newVoteValue > 0)
                    {
                        newVoteType = 'upvote';
                        btnSelected = upScore.parentElement;
                    }
                    else
                    {
                        newVoteType = 'downvote';
                        btnSelected = downScore.parentElement;
                    }

                    // update classes
                    document.querySelectorAll('button[data-votetype]').forEach((el) => el.classList.remove('voted'));
                    btnSelected.classList.add('voted');

                    // check duplicate vote
                    if (existingVote && existingVote.dataset.votetype === newVoteType)
                    {
                        return;
                    }

                    downScore.innerText = response.getAttribute('neg');
                    upScore.innerText = response.getAttribute('pos');
                }
                if (!skipSlide)
                {
                    score.slideDown("normal");
                }
            }
            else
            {
                if (show_error)
                {
                    var errorText = $(data).find("error").text();
                    if (errorText == 'Unverified Email' && $("#unverifiedEmailError").length > 0)
                    {
                        unverifiedEmailDialog();
                    }
                    else if (errorText == 'Unverified Email' && controltype == "mobile3")
                    {
                        resendValidationModal.show();
                    }
                    else if (typeof(errordialog) == "function")
                    {
                        errordialog(errorText, null, 300);
                    }
                    else
                    {
                        alert(errorText);
                    }
                }
            }
        });

        return false;
    }

    sdwndw.apply_threadrate_styles = function(postid, vote, votepos, voteneg, score)
    {
        var threadrating = $("[role='threadrating'][data-postid='" + postid + "']");
        var pos = threadrating.find("[role='button'][data-vote='1']");
        var neg = threadrating.find("[role='button'][data-vote='-1']");
        var voted = vote > 0 ? pos : neg;

        if (vote === 0)
        {
            threadrating.removeClass("votepos voteneg");
            threadrating.find("[role='button']").removeClass("voted");
        }
        else
        {
            if (vote > 0)
            {
                threadrating.removeClass("voteneg").addClass("votepos");
            } else if (vote < 0)
            {
                threadrating.removeClass("votepos").addClass("voteneg");
            }

            threadrating.find("[role='button']").removeClass("voted");
            voted.addClass("voted");
        }

        if (score < 0)
        {
            threadrating.removeClass("scorepos").addClass("scoreneg");
        }
        else
        {
            threadrating.removeClass("scoreneg").addClass("scorepos");
        }

        if (votepos)
        {
            pos.find("[role='value']").text(votepos);
        }
        if (voteneg)
        {
            neg.find("[role='value']").text(voteneg);
        }

        threadrating.removeClass("hover");
    }

    sdwndw.sdthreadrate_highlightfp = function(threadids)
    {
        $.post("/ajax/threadrate.php", {
            'ti': threadids,
            'securitytoken': SECURITYTOKEN
        }, function(data)
        {
            $.each(data, function(index, value)
            {
                var el = $('[data-threadid="' + index + '"] [role="threadrating"]');

                if (value == "up")
                {
                    el.addClass('votepos');
                }
                else if (value == "down")
                {
                    el.addClass('voteneg');
                }

                // TODO: https://slickdeals.atlassian.net/browse/SLICKDEALS-63963
                const threadList = document.querySelectorAll('.js-votingThumbs--' + index);
                threadList.forEach(function (voteElement) {
                    const voteEvent = new CustomEvent('vote', {
                        detail: {
                            vote: value
                        }
                    });
                    voteElement.dispatchEvent(voteEvent);
                });

                /**
                 * This functionality is tied to blueprint/src/javascript/votingThumbsPopup.ts
                 */
                const votingThumbsPopupElement = document.querySelector(`.js-votingThumbsPopup[data-threadId="${index}"]`);

                if (votingThumbsPopupElement) {
                    votingThumbsPopupElement.dataset.votingType = value;

                    const voteEvent = new CustomEvent('userVoted', {
                        detail: {
                            voteType: value
                        }
                    });

                    votingThumbsPopupElement.dispatchEvent(voteEvent);
                }
            });
        });
    }

    // Assign to function so that it can be overridden in another layer to deal with load order issues (esp. in Firefox).
    if (!window.threadRateOnReady)
    {
        window.threadRateOnReady = function ()
        {
            $(document).on('click', '[role="threadrating"] [role="button"]', function (ev)
            {
                var threadrate = $(this).closest("[role='threadrating']");
                var ibr = $(this).closest(".itemBottomRow, .content");
                var firstPostThumbs = ($(this).parent().data('role') === "firstThumbsForFirstPost");

                var vote = $(this).data('vote');

                if ($(this).hasClass("voted"))
                {
                    vote = 0;
                }

                var dropdownId = $(this).data('dropdown');
                if (dropdownId)
                {
                    $(dropdownId).find("input").attr("action_source", threadrate.data("actionSource"));
                }

                if (vote === 0 && dropdownId) {
                    $(dropdownId).hide();
                }

                var controltype = "modern";
                if ($(this).data("controltype"))
                {
                    controltype = $(this).data("controltype");
                }
                if (ibr.length === 0 || !ibr.data("user_is_touching") || !ibr.data("user_is_entering"))
                {
                    if (threadrate.data('threadtitle'))
                    {
                        SD.Analytics.addDataLayerProductTitle(threadrate.data('threadtitle'));
                    }
                    sdthreadrate_vote(
                        threadrate.data('postid'),
                        vote,
                        controltype,
                        threadrate.data('forumid'),
                        firstPostThumbs,
                        threadrate.data('actionSource')
                    );
                }
                ibr.data("user_is_entering", false);
                ibr.data("user_is_touching", false);
            }).on("mouseenter", "[role='threadrating']", function (ev)
            {
                $(".hover[role='threadrating']").removeClass("hover");
                $(this).addClass('hover');
            });

            if (window.navigator.msPointerEnabled)
            {
                $(document).on("click", function (ev)
                {
                    if ($(ev.target).closest("[role='threadrating']").length === 0)
                    {
                        $(".hover[role='threadrating']").removeClass("hover");
                    }
                });
            }
            else
            {
                $(document).on("mouseleave", "[role='threadrating']", function (ev)
                {
                    $(this).removeClass('hover');
                });
            }

            $(document).on("mouseenter", "[role='threadrating']", function (ev)
            {
                $(this).find("[role='button'][data-dropdown]").each(function ()
                {
                    if ($($(this).data("dropdown")).find("label[for]").length < 2)
                    {
                        $(this).removeAttr("data-dropdown");
                    }
                });
            });
        };
    };

    $(document).ready(window.threadRateOnReady);
})(window);

;/*
 * @name BeautyTips
 * @desc a tooltips/baloon-help plugin for jQuery
 *
 * @author Jeff Robbins - Lullabot - http://www.lullabot.com
 * @version 0.9.5 release candidate 1  (5/20/2009)
 */

jQuery.bt = {version: '0.9.5-rc1'};

/*
 * @type jQuery
 * @cat Plugins/bt
 * @requires jQuery v1.2+ (not tested on versions prior to 1.2.6)
 *
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 * Encourage development. If you use BeautyTips for anything cool
 * or on a site that people have heard of, please drop me a note.
 * - jeff ^at lullabot > com
 *
 * No guarantees, warranties, or promises of any kind
 *
 */

;(function($) {
  /**
   * @credit Inspired by Karl Swedberg's ClueTip
   *    (http://plugins.learningjquery.com/cluetip/), which in turn was inspired
   *    by Cody Lindley's jTip (http://www.codylindley.com)
   *
   * @fileoverview
   * Beauty Tips is a jQuery tooltips plugin which uses the canvas drawing element
   * in the HTML5 spec in order to dynamically draw tooltip "talk bubbles" around
   * the descriptive help text associated with an item. This is in many ways
   * similar to Google Maps which both provides similar talk-bubbles and uses the
   * canvas element to draw them.
   *
   * The canvas element is supported in modern versions of FireFox, Safari, and
   * Opera. However, Internet Explorer needs a separate library called ExplorerCanvas
   * included on the page in order to support canvas drawing functions. ExplorerCanvas
   * was created by Google for use with their web apps and you can find it here:
   * http://excanvas.sourceforge.net/
   *
   * Beauty Tips was written to be simple to use and pretty. All of its options
   * are documented at the bottom of this file and defaults can be overwritten
   * globally for the entire page, or individually on each call.
   *
   * By default each tooltip will be positioned on the side of the target element
   * which has the most free space. This is affected by the scroll position and
   * size of the current window, so each Beauty Tip is redrawn each time it is
   * displayed. It may appear above an element at the bottom of the page, but when
   * the page is scrolled down (and the element is at the top of the page) it will
   * then appear below it. Additionally, positions can be forced or a preferred
   * order can be defined. See examples below.
   *
   * To fix z-index problems in IE6, include the bgiframe plugin on your page
   * http://plugins.jquery.com/project/bgiframe - BeautyTips will automatically
   * recognize it and use it.
   *
   * BeautyTips also works with the hoverIntent plugin
   * http://cherne.net/brian/resources/jquery.hoverIntent.html
   * see hoverIntent example below for usage
   *
   * Usage
   * The function can be called in a number of ways.
   * $(selector).bt();
   * $(selector).bt('Content text');
   * $(selector).bt('Content text', {option1: value, option2: value});
   * $(selector).bt({option1: value, option2: value});
   *
   * For more/better documentation and lots of examples, visit the demo page included with the distribution
   *
   */

  jQuery.fn.bt = function(content, options) {

    if (typeof content != 'string') {
      var contentSelect = true;
      options = content;
      content = false;
    }
    else {
      var contentSelect = false;
    }

    // if hoverIntent is installed, use that as default instead of hover
    if (jQuery.fn.hoverIntent && jQuery.bt.defaults.trigger == 'hover') {
      jQuery.bt.defaults.trigger = 'hoverIntent';
    }

    return this.each(function(index) {

      var opts = jQuery.extend(false, jQuery.bt.defaults, jQuery.bt.options, options);

      // clean up the options
      opts.spikeLength = numb(opts.spikeLength);
      opts.spikeGirth = numb(opts.spikeGirth);
      opts.overlap = numb(opts.overlap);

      var ajaxTimeout = false;

      /**
       * This is sort of the "starting spot" for the this.each()
       * These are the init functions to handle the .bt() call
       */

      if (opts.killTitle) {
        $(this).find('[title]').andSelf().each(function() {
          if (!$(this).attr('bt-xTitle')) {
            var el = $(this).attr('bt-xTitle', $(this).attr('title'));
            if (el)
            {
              el.attr('title', '');
            }
          }
        });
      }

      if (typeof opts.trigger == 'string') {
        opts.trigger = [opts.trigger];
      }
      if (opts.trigger[0] == 'hoverIntent') {
        var hoverOpts = jQuery.extend(opts.hoverIntentOpts, {
          over: function() {
            this.btOn();
          },
          out: function() {
            this.btOff();
          }});
        $(this).hoverIntent(hoverOpts);

      }
      else if (opts.trigger[0] == 'hover') {
        $(this).hover(
          function() {
            this.btOn();
          },
          function() {
            this.btOff();
          }
        );
      }
      else if (opts.trigger[0] == 'now') {
        // toggle the on/off right now
        // note that 'none' gives more control (see below)
        if ($(this).hasClass('bt-active')) {
          this.btOff();
        }
        else {
          this.btOn();
        }
      }
      else if (opts.trigger[0] == 'none') {
        // initialize the tip with no event trigger
        // use javascript to turn on/off tip as follows:
        // $('#selector').btOn();
        // $('#selector').btOff();
      }
      else if (opts.trigger.length > 1 && opts.trigger[0] != opts.trigger[1]) {
        $(this)
          .bind(opts.trigger[0], function() {
            this.btOn();
          })
          .bind(opts.trigger[1], function() {
            this.btOff();
          });
      }
      else {
        // toggle using the same event
        $(this).bind(opts.trigger[0], function() {
          if ($(this).hasClass('bt-active')) {
            this.btOff();
          }
          else {
            this.btOn();
          }
        });
      }


      /**
       *  The BIG TURN ON
       *  Any element that has been initiated
       */
      this.btOn = function () {
        if (typeof $(this).data('bt-box') == 'object') {
          // if there's already a popup, remove it before creating a new one.
          this.btOff();
        }

        // trigger preBuild function
        // preBuild has no argument since the box hasn't been built yet
        opts.preBuild.apply(this);

        // turn off other tips
        $(jQuery.bt.vars.closeWhenOpenStack).btOff();

        // add the class to the target element (for hilighting, for example)
        // bt-active is always applied to all, but activeClass can apply another
        $(this).addClass('bt-active ' + opts.activeClass);

        if (contentSelect && opts.ajaxPath == null) {
          // bizarre, I know
          if (opts.killTitle) {
            // if we've killed the title attribute, it's been stored in 'bt-xTitle' so get it..
            $(this).attr('title', $(this).attr('bt-xTitle'));
          }
          // then evaluate the selector... title is now in place
          content = $.isFunction(opts.contentSelector) ? opts.contentSelector.apply(this) : eval(opts.contentSelector);
          if (opts.killTitle) {
            // now remove the title again, so we don't get double tips
            $(this).attr('title', '');
          }
        }

        // ----------------------------------------------
        // All the Ajax(ish) stuff is in this next bit...
        // ----------------------------------------------
        if (opts.ajaxPath != null && content == false) {
          if (typeof opts.ajaxPath == 'object') {
            var url = eval(opts.ajaxPath[0]);
            url += opts.ajaxPath[1] ? ' ' + opts.ajaxPath[1] : '';
          }
          else {
            var url = opts.ajaxPath;
          }
          var off = url.indexOf(" ");
          if ( off >= 0 ) {
            var selector = url.slice(off, url.length);
            url = url.slice(0, off);
          }

          // load any data cached for the given ajax path
          var cacheData = opts.ajaxCache ? $(document.body).data('btCache-' + url.replace(/\./g, '')) : null;
          if (typeof cacheData == 'string') {
            content = selector ? $("<div/>").append(cacheData.replace(/<script(.|\s)*?\/script>/g, "")).find(selector) : cacheData;
          }
          else {
            var target = this;

            // set up the options
            var ajaxOpts = jQuery.extend(false,
            {
              type: opts.ajaxType,
              data: opts.ajaxData,
              cache: opts.ajaxCache,
              url: url,
              complete: function(XMLHttpRequest, textStatus) {
                if (textStatus == 'success' || textStatus == 'notmodified') {
                  if (opts.ajaxCache) {
                    $(document.body).data('btCache-' + url.replace(/\./g, ''), XMLHttpRequest.responseText);
                  }
                  ajaxTimeout = false;
                  content = selector ?
                    // Create a dummy div to hold the results
                    $("<div/>")
                      // inject the contents of the document in, removing the scripts
                      // to avoid any 'Permission Denied' errors in IE
                      .append(XMLHttpRequest.responseText.replace(/<script(.|\s)*?\/script>/g, ""))

                      // Locate the specified elements
                      .find(selector) :

                    // If not, just inject the full result
                    XMLHttpRequest.responseText;

                }
                else {
                  if (textStatus == 'timeout') {
                    // if there was a timeout, we don't cache the result
                    ajaxTimeout = true;
                  }
                  content = opts.ajaxError.replace(/%error/g, XMLHttpRequest.statusText);
                }
                // if the user rolls out of the target element before the ajax request comes back, don't show it
                if ($(target).hasClass('bt-active')) {
                  target.btOn();
                }
              }
            }, opts.ajaxOpts);
            // do the ajax request
            jQuery.ajax(ajaxOpts);
            // load the throbber while the magic happens
            content = opts.ajaxLoading;
          }
        }
        // </ ajax stuff >


        // now we start actually figuring out where to place the tip

        // figure out how to compensate for the shadow, if present
        var shadowMarginX = 0; // extra added to width to compensate for shadow
        var shadowMarginY = 0; // extra added to height
        var shadowShiftX = 0;  // amount to shift the tip horizontally to allow for shadow
        var shadowShiftY = 0;  // amount to shift vertical

        if (opts.shadow && !shadowSupport()) {
          // if browser doesn't support drop shadows, turn them off
          opts.shadow = false;
          // and bring in the noShadows options
          jQuery.extend(opts, opts.noShadowOpts);
        }

        if (opts.shadow) {
          // figure out horizontal placement
          if (opts.shadowBlur > Math.abs(opts.shadowOffsetX)) {
            shadowMarginX = opts.shadowBlur * 2;
          }
          else {
            shadowMarginX = opts.shadowBlur + Math.abs(opts.shadowOffsetX);
          }
          shadowShiftX = (opts.shadowBlur - opts.shadowOffsetX) > 0 ? opts.shadowBlur - opts.shadowOffsetX : 0;

          // now vertical
          if (opts.shadowBlur > Math.abs(opts.shadowOffsetY)) {
            shadowMarginY = opts.shadowBlur * 2;
          }
          else {
            shadowMarginY = opts.shadowBlur + Math.abs(opts.shadowOffsetY);
          }
          shadowShiftY = (opts.shadowBlur - opts.shadowOffsetY) > 0 ? opts.shadowBlur - opts.shadowOffsetY : 0;
        }

        if (opts.offsetParent){
          // if offsetParent is defined by user
          var offsetParent = $(opts.offsetParent);
          var offsetParentPos = offsetParent.offset();
          var pos = $(this).offset();
          var top = numb(pos.top) - numb(offsetParentPos.top) + numb($(this).css('margin-top')) - shadowShiftY; // IE can return 'auto' for margins
          var left = numb(pos.left) - numb(offsetParentPos.left) + numb($(this).css('margin-left')) - shadowShiftX;
        }
        else {
          // if the target element is absolutely positioned, use its parent's offsetParent instead of its own
          var offsetParent = ($(this).css('position') == 'absolute') ? $(this).parents().eq(0).offsetParent() : $(this).offsetParent();
          var pos = $(this).btPosition();
          var top = numb(pos.top) + numb($(this).css('margin-top')) - shadowShiftY; // IE can return 'auto' for margins
          var left = numb(pos.left) + numb($(this).css('margin-left')) - shadowShiftX;
        }

        var width = $(this).btOuterWidth();
        var height = $(this).outerHeight();

        if (typeof content == 'object') {
          // if content is a DOM object (as opposed to text)
          // use a clone, rather than removing the original element
          // and ensure that it's visible
          var original = content;
          var clone = $(original).clone(true).show();
          // also store a reference to the original object in the clone data
          // and a reference to the clone in the original
          var origClones = $(original).data('bt-clones') || [];
          origClones.push(clone);
          $(original).data('bt-clones', origClones);
          $(clone).data('bt-orig', original);
          $(this).data('bt-content-orig', {original: original, clone: clone});
          content = clone;
        }
        if (typeof content == 'null' || content == '') {
          // if content is empty, bail out...
          return;
        }

        // create the tip content div, populate it, and style it
        var $text = $('<div class="bt-content"></div>').append(content).css({padding: opts.padding, position: 'absolute', width: (opts.shrinkToFit ? 'auto' : opts.width), zIndex: opts.textzIndex, left: shadowShiftX, top: shadowShiftY}).css(opts.cssStyles);
        // create the wrapping box which contains text and canvas
        // put the content in it, style it, and append it to the same offset parent as the target
        var $box = $('<div class="bt-wrapper"></div>').append($text).addClass(opts.cssClass).css({position: 'absolute', width: opts.width, zIndex: opts.wrapperzIndex, visibility:'hidden'}).appendTo(offsetParent);

        // use bgiframe to get around z-index problems in IE6
        // http://plugins.jquery.com/project/bgiframe
        if (jQuery.fn.bgiframe) {
          $text.bgiframe();
          $box.bgiframe();
        }

        $(this).data('bt-box', $box);

        // see if the text box will fit in the various positions
        var scrollTop = numb($(document).scrollTop());
        var scrollLeft = numb($(document).scrollLeft());
        var docWidth = numb($(window).width());
        var docHeight = numb($(window).height());
        var winRight = scrollLeft + docWidth;
        var winBottom = scrollTop + docHeight;
        var space = new Object();
        var thisOffset = $(this).offset();
        space.top = thisOffset.top - scrollTop;
        space.bottom = docHeight - ((thisOffset + height) - scrollTop);
        space.left = thisOffset.left - scrollLeft;
        space.right = docWidth - ((thisOffset.left + width) - scrollLeft);
        var textOutHeight = numb($text.outerHeight());
        var textOutWidth = numb($text.btOuterWidth());
        if (opts.positions.constructor == String) {
          opts.positions = opts.positions.replace(/ /, '').split(',');
        }
        if (opts.positions[0] == 'most') {
          // figure out which is the largest
          var position = 'top'; // prime the pump
          for (var pig in space) {  //            <-------  pigs in space!
            position = space[pig] > space[position] ? pig : position;
          }
        }
        else {
          for (var x in opts.positions) {
            var position = opts.positions[x];
            // @todo: acommodate shadow space in the following lines...
            if ((position == 'left' || position == 'right') && space[position] > textOutWidth + opts.spikeLength) {
              break;
            }
            else if ((position == 'top' || position == 'bottom') && space[position] > textOutHeight + opts.spikeLength) {
              break;
            }
          }
        }

        // horizontal (left) offset for the box
        var horiz = left + ((width - textOutWidth) * .5);
        // vertical (top) offset for the box
        var vert = top + ((height - textOutHeight) * .5);
        var points = [];
        var textTop, textLeft, textRight, textBottom, textTopSpace, textBottomSpace, textLeftSpace, textRightSpace, crossPoint, textCenter, spikePoint;

        // Yes, yes, this next bit really could use to be condensed
        // each switch case is basically doing the same thing in slightly different ways
        switch(position) {

          // =================== TOP =======================
          case 'top':
            // spike on bottom
            $text.css('margin-bottom', opts.spikeLength + 'px');
            $box.css({top: (top - $text.outerHeight(true)) + opts.overlap, left: horiz});
            // move text left/right if extends out of window
            textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.btOuterWidth(true));
            var xShift = shadowShiftX;
            if (textRightSpace < 0) {
              // shift it left
              $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px');
              xShift -= textRightSpace;
            }
            // we test left space second to ensure that left of box is visible
            textLeftSpace = ($text.offset().left + numb($text.css('margin-left'))) - (scrollLeft + opts.windowMargin);
            if (textLeftSpace < 0) {
              // shift it right
              $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px');
              xShift += textLeftSpace;
            }
            textTop = $text.btPosition().top + numb($text.css('margin-top'));
            textLeft = $text.btPosition().left + numb($text.css('margin-left'));
            textRight = textLeft + $text.btOuterWidth();
            textBottom = textTop + $text.outerHeight();
            textCenter = {x: textLeft + ($text.btOuterWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)};
            // points[points.length] = {x: x, y: y};
            points[points.length] = spikePoint = {y: textBottom + opts.spikeLength, x: ((textRight-textLeft) * .5) + xShift, type: 'spike'};
            crossPoint = findIntersectX(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textBottom);
            // make sure that the crossPoint is not outside of text box boundaries
            crossPoint.x = crossPoint.x < textLeft + opts.spikeGirth/2 + opts.cornerRadius ? textLeft + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.x;
            crossPoint.x =  crossPoint.x > (textRight - opts.spikeGirth/2) - opts.cornerRadius ? (textRight - opts.spikeGirth/2) - opts.CornerRadius : crossPoint.x;
            points[points.length] = {x: crossPoint.x - (opts.spikeGirth/2), y: textBottom, type: 'join'};
            points[points.length] = {x: textLeft, y: textBottom, type: 'corner'};  // left bottom corner
            points[points.length] = {x: textLeft, y: textTop, type: 'corner'};     // left top corner
            points[points.length] = {x: textRight, y: textTop, type: 'corner'};    // right top corner
            points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner
            points[points.length] = {x: crossPoint.x + (opts.spikeGirth/2), y: textBottom, type: 'join'};
            points[points.length] = spikePoint;
            break;

          // =================== LEFT =======================
          case 'left':
            // spike on right
            $text.css('margin-right', opts.spikeLength + 'px');
            $box.css({top: vert + 'px', left: ((left - $text.btOuterWidth(true)) + opts.overlap) + 'px'});
            // move text up/down if extends out of window
            textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true));
            var yShift = shadowShiftY;
            if (textBottomSpace < 0) {
              // shift it up
              $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px');
              yShift -= textBottomSpace;
            }
            // we ensure top space second to ensure that top of box is visible
            textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin);
            if (textTopSpace < 0) {
              // shift it down
              $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px');
              yShift += textTopSpace;
            }
            textTop = $text.btPosition().top + numb($text.css('margin-top'));
            textLeft = $text.btPosition().left + numb($text.css('margin-left'));
            textRight = textLeft + $text.btOuterWidth();
            textBottom = textTop + $text.outerHeight();
            textCenter = {x: textLeft + ($text.btOuterWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)};
            points[points.length] = spikePoint = {x: textRight + opts.spikeLength, y: ((textBottom-textTop) * .5) + yShift, type: 'spike'};
            crossPoint = findIntersectY(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textRight);
            // make sure that the crossPoint is not outside of text box boundaries
            crossPoint.y = crossPoint.y < textTop + opts.spikeGirth/2 + opts.cornerRadius ? textTop + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.y;
            crossPoint.y =  crossPoint.y > (textBottom - opts.spikeGirth/2) - opts.cornerRadius ? (textBottom - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.y;
            points[points.length] = {x: textRight, y: crossPoint.y + opts.spikeGirth/2, type: 'join'};
            points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner
            points[points.length] = {x: textLeft, y: textBottom, type: 'corner'};  // left bottom corner
            points[points.length] = {x: textLeft, y: textTop, type: 'corner'};     // left top corner
            points[points.length] = {x: textRight, y: textTop, type: 'corner'};    // right top corner
            points[points.length] = {x: textRight, y: crossPoint.y - opts.spikeGirth/2, type: 'join'};
            points[points.length] = spikePoint;
            break;

          // =================== BOTTOM =======================
          case 'bottom':
            // spike on top
            $text.css('margin-top', opts.spikeLength + 'px');
            $box.css({top: (top + height) - opts.overlap, left: horiz});
            // move text up/down if extends out of window
            textRightSpace = (winRight - opts.windowMargin) - ($text.offset().left + $text.btOuterWidth(true));
            var xShift = shadowShiftX;
            if (textRightSpace < 0) {
              // shift it left
              $box.css('left', (numb($box.css('left')) + textRightSpace) + 'px');
              xShift -= textRightSpace;
            }
            // we ensure left space second to ensure that left of box is visible
            textLeftSpace = ($text.offset().left + numb($text.css('margin-left')))  - (scrollLeft + opts.windowMargin);
            if (textLeftSpace < 0) {
              // shift it right
              $box.css('left', (numb($box.css('left')) - textLeftSpace) + 'px');
              xShift += textLeftSpace;
            }
            textTop = $text.btPosition().top + numb($text.css('margin-top'));
            textLeft = $text.btPosition().left + numb($text.css('margin-left'));
            textRight = textLeft + $text.btOuterWidth();
            textBottom = textTop + $text.outerHeight();
            textCenter = {x: textLeft + ($text.btOuterWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)};
            points[points.length] = spikePoint = {x: ((textRight-textLeft) * .5) + xShift, y: shadowShiftY, type: 'spike'};
            crossPoint = findIntersectX(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textTop);
            // make sure that the crossPoint is not outside of text box boundaries
            crossPoint.x = crossPoint.x < textLeft + opts.spikeGirth/2 + opts.cornerRadius ? textLeft + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.x;
            crossPoint.x =  crossPoint.x > (textRight - opts.spikeGirth/2) - opts.cornerRadius ? (textRight - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.x;
            points[points.length] = {x: crossPoint.x + opts.spikeGirth/2, y: textTop, type: 'join'};
            points[points.length] = {x: textRight, y: textTop, type: 'corner'};    // right top corner
            points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner
            points[points.length] = {x: textLeft, y: textBottom, type: 'corner'};  // left bottom corner
            points[points.length] = {x: textLeft, y: textTop, type: 'corner'};     // left top corner
            points[points.length] = {x: crossPoint.x - (opts.spikeGirth/2), y: textTop, type: 'join'};
            points[points.length] = spikePoint;
            break;

          // =================== RIGHT =======================
          case 'right':
            // spike on left
            $text.css('margin-left', (opts.spikeLength + 'px'));
            $box.css({top: vert + 'px', left: ((left + width) - opts.overlap) + 'px'});
            // move text up/down if extends out of window
            textBottomSpace = (winBottom - opts.windowMargin) - ($text.offset().top + $text.outerHeight(true));
            var yShift = shadowShiftY;
            if (textBottomSpace < 0) {
              // shift it up
              $box.css('top', (numb($box.css('top')) + textBottomSpace) + 'px');
              yShift -= textBottomSpace;
            }
            // we ensure top space second to ensure that top of box is visible
            textTopSpace = ($text.offset().top + numb($text.css('margin-top'))) - (scrollTop + opts.windowMargin);
            if (textTopSpace < 0) {
              // shift it down
              $box.css('top', (numb($box.css('top')) - textTopSpace) + 'px');
              yShift += textTopSpace;
            }
            textTop = $text.btPosition().top + numb($text.css('margin-top'));
            textLeft = $text.btPosition().left + numb($text.css('margin-left'));
            textRight = textLeft + $text.btOuterWidth();
            textBottom = textTop + $text.outerHeight();
            textCenter = {x: textLeft + ($text.btOuterWidth()*opts.centerPointX), y: textTop + ($text.outerHeight()*opts.centerPointY)};
            points[points.length] = spikePoint = {x: shadowShiftX, y: ((textBottom-textTop) * .5) + yShift, type: 'spike'};
            crossPoint = findIntersectY(spikePoint.x, spikePoint.y, textCenter.x, textCenter.y, textLeft);
            // make sure that the crossPoint is not outside of text box boundaries
            crossPoint.y = crossPoint.y < textTop + opts.spikeGirth/2 + opts.cornerRadius ? textTop + opts.spikeGirth/2 + opts.cornerRadius : crossPoint.y;
            crossPoint.y =  crossPoint.y > (textBottom - opts.spikeGirth/2) - opts.cornerRadius ? (textBottom - opts.spikeGirth/2) - opts.cornerRadius : crossPoint.y;
            points[points.length] = {x: textLeft, y: crossPoint.y - opts.spikeGirth/2, type: 'join'};
            points[points.length] = {x: textLeft, y: textTop, type: 'corner'};     // left top corner
            points[points.length] = {x: textRight, y: textTop, type: 'corner'};    // right top corner
            points[points.length] = {x: textRight, y: textBottom, type: 'corner'}; // right bottom corner
            points[points.length] = {x: textLeft, y: textBottom, type: 'corner'};  // left bottom corner
            points[points.length] = {x: textLeft, y: crossPoint.y + opts.spikeGirth/2, type: 'join'};
            points[points.length] = spikePoint;
            break;
        } // </ switch >

        var canvas = document.createElement('canvas');
        $(canvas).attr('width', (numb($text.btOuterWidth(true)) + opts.strokeWidth*2 + shadowMarginX)).attr('height', (numb($text.outerHeight(true)) + opts.strokeWidth*2 + shadowMarginY)).appendTo($box).css({position: 'absolute', zIndex: opts.boxzIndex});


        // if excanvas is set up, we need to initialize the new canvas element
        if (typeof G_vmlCanvasManager != 'undefined') {
          canvas = G_vmlCanvasManager.initElement(canvas);
        }

        if (opts.cornerRadius > 0) {
          // round the corners!
          var newPoints = [];
          var newPoint;
          for (var i=0; i<points.length; i++) {
            if (points[i].type == 'corner') {
              // create two new arc points
              // find point between this and previous (using modulo in case of ending)
              newPoint = betweenPoint(points[i], points[(i-1)%points.length], opts.cornerRadius);
              newPoint.type = 'arcStart';
              newPoints[newPoints.length] = newPoint;
              // the original corner point
              newPoints[newPoints.length] = points[i];
              // find point between this and next
              newPoint = betweenPoint(points[i], points[(i+1)%points.length], opts.cornerRadius);
              newPoint.type = 'arcEnd';
              newPoints[newPoints.length] = newPoint;
            }
            else {
              newPoints[newPoints.length] = points[i];
            }
          }
          // overwrite points with new version
          points = newPoints;
        }

        var ctx = canvas.getContext("2d");

        if (opts.shadow && opts.shadowOverlap !== true) {

          var shadowOverlap = numb(opts.shadowOverlap);

          // keep the shadow (and canvas) from overlapping the target element
          switch (position) {
            case 'top':
              if (opts.shadowOffsetX + opts.shadowBlur - shadowOverlap > 0) {
                $box.css('top', (numb($box.css('top')) - (opts.shadowOffsetX + opts.shadowBlur - shadowOverlap)));
              }
              break;
            case 'right':
              if (shadowShiftX - shadowOverlap > 0) {
                $box.css('left', (numb($box.css('left')) + shadowShiftX - shadowOverlap));
              }
              break;
            case 'bottom':
              if (shadowShiftY - shadowOverlap > 0) {
                $box.css('top', (numb($box.css('top')) + shadowShiftY - shadowOverlap));
              }
              break;
            case 'left':
              if (opts.shadowOffsetY + opts.shadowBlur - shadowOverlap > 0) {
                $box.css('left', (numb($box.css('left')) - (opts.shadowOffsetY + opts.shadowBlur - shadowOverlap)));
              }
              break;
          }
        }

        drawIt.apply(ctx, [points], opts.strokeWidth);
        ctx.fillStyle = opts.fill;
        if (opts.shadow) {
          ctx.shadowOffsetX = opts.shadowOffsetX;
          ctx.shadowOffsetY = opts.shadowOffsetY;
          ctx.shadowBlur = opts.shadowBlur;
          ctx.shadowColor =  opts.shadowColor;
        }
        ctx.closePath();
        ctx.fill();
        if (opts.strokeWidth > 0) {
          ctx.shadowColor = 'rgba(0, 0, 0, 0)'; //remove shadow from stroke
          ctx.lineWidth = opts.strokeWidth;
          ctx.strokeStyle = opts.strokeStyle;
          ctx.beginPath();
          drawIt.apply(ctx, [points], opts.strokeWidth);
          ctx.closePath();
          ctx.stroke();
        }

        // trigger preShow function
        // function receives the box element (the balloon wrapper div) as an argument
        opts.preShow.apply(this, [$box[0]]);

        // switch from visibility: hidden to display: none so we can run animations
        $box.css({display:'none', visibility: 'visible'});

        // Here's where we show the tip
        opts.showTip.apply(this, [$box[0]]);

        if (opts.overlay) {
          // EXPERIMENTAL AND FOR TESTING ONLY!!!!
          var overlay = $('<div class="bt-overlay"></div>').css({
              position: 'absolute',
              backgroundColor: 'blue',
              top: top,
              left: left,
              width: width,
              height: height,
              opacity: '.2'
            }).appendTo(offsetParent);
          $(this).data('overlay', overlay);
        }

        if ((opts.ajaxPath != null && opts.ajaxCache == false) || ajaxTimeout) {
          // if ajaxCache is not enabled or if there was a server timeout,
          // remove the content variable so it will be loaded again from server
          content = false;
        }

        // stick this element into the clickAnywhereToClose stack
        if (opts.clickAnywhereToClose) {
          jQuery.bt.vars.clickAnywhereStack.push(this);
          $(document).click(jQuery.bt.docClick);
        }

        // stick this element into the closeWhenOthersOpen stack
        if (opts.closeWhenOthersOpen) {
          jQuery.bt.vars.closeWhenOpenStack.push(this);
        }

        // trigger postShow function
        // function receives the box element (the balloon wrapper div) as an argument
        opts.postShow.apply(this, [$box[0]]);


      }; // </ turnOn() >

      this.btOff = function() {

        var box = $(this).data('bt-box');

        // trigger preHide function
        // function receives the box element (the balloon wrapper div) as an argument
        opts.preHide.apply(this, [box]);

        var i = this;

        // set up the stuff to happen AFTER the tip is hidden
        i.btCleanup = function(){
          var box = $(i).data('bt-box');
          var contentOrig = $(i).data('bt-content-orig');
          var overlay = $(i).data('bt-overlay');
          if (typeof box == 'object') {
            $(box).remove();
            $(i).removeData('bt-box');
          }
          if (typeof contentOrig == 'object') {
            var clones = $(contentOrig.original).data('bt-clones');
            $(contentOrig).data('bt-clones', arrayRemove(clones, contentOrig.clone));
          }
          if (typeof overlay == 'object') {
            $(overlay).remove();
            $(i).removeData('bt-overlay');
          }

          // remove this from the stacks
          jQuery.bt.vars.clickAnywhereStack = arrayRemove(jQuery.bt.vars.clickAnywhereStack, i);
          jQuery.bt.vars.closeWhenOpenStack = arrayRemove(jQuery.bt.vars.closeWhenOpenStack, i);

          // remove the 'bt-active' and activeClass classes from target
          $(i).removeClass('bt-active ' + opts.activeClass);

          // trigger postHide function
          // no box argument since it has been removed from the DOM
          opts.postHide.apply(i);

        }

        opts.hideTip.apply(this, [box, i.btCleanup]);

      }; // </ turnOff() >

      var refresh = this.btRefresh = function() {
        this.btOff();
        this.btOn();
      };

    }); // </ this.each() >


    function drawIt(points, strokeWidth) {
      this.moveTo(points[0].x, points[0].y);
      for (i=1;i<points.length;i++) {
        if (points[i-1].type == 'arcStart') {
          // if we're creating a rounded corner
          //ctx.arc(round5(points[i].x), round5(points[i].y), points[i].startAngle, points[i].endAngle, opts.cornerRadius, false);
          this.quadraticCurveTo(round5(points[i].x, strokeWidth), round5(points[i].y, strokeWidth), round5(points[(i+1)%points.length].x, strokeWidth), round5(points[(i+1)%points.length].y, strokeWidth));
          i++;
          //ctx.moveTo(round5(points[i].x), round5(points[i].y));
        }
        else {
          this.lineTo(round5(points[i].x, strokeWidth), round5(points[i].y, strokeWidth));
        }
      }
    }; // </ drawIt() >

    /**
     * For odd stroke widths, round to the nearest .5 pixel to avoid antialiasing
     * http://developer.mozilla.org/en/Canvas_tutorial/Applying_styles_and_colors
     */
    function round5(num, strokeWidth) {
      var ret;
      strokeWidth = numb(strokeWidth);
      if (strokeWidth%2) {
        ret = num;
      }
      else {
        ret = Math.round(num - .5) + .5;
      }
      return ret;
    }; // </ round5() >

    /**
     * Ensure that a number is a number... or zero
     */
    function numb(num) {
      return parseInt(num) || 0;
    }; // </ numb() >

    /**
     * Remove an element from an array
     */
    function arrayRemove(arr, elem) {
      var x, newArr = [];
      for (x in arr) {
        if (arr[x] != elem) {
          newArr.push(arr[x]);
        }
      }
      return newArr;
    }; // </ arrayRemove() >

    /**
     * Does the current browser support canvas?
     * This is a variation of http://code.google.com/p/browser-canvas-support/
     */
    function canvasSupport() {
      var canvas_compatible = false;
      try {
        canvas_compatible = !!(document.createElement('canvas').getContext('2d')); // S60
      } catch(e) {
        canvas_compatible = !!(document.createElement('canvas').getContext); // IE
      }
      return canvas_compatible;
    }

    /**
     * Does the current browser support canvas drop shadows?
     */
    function shadowSupport() {

      // to test for drop shadow support in the current browser, uncomment the next line
      // return true;

      // until a good feature-detect is found, we have to look at user agents
      try {
        var userAgent = navigator.userAgent.toLowerCase();
        if (/webkit/.test(userAgent)) {
          // WebKit.. let's go!
          return true;
        }
        else if (/gecko|mozilla/.test(userAgent) && parseFloat(userAgent.match(/firefox\/(\d+(?:\.\d+)+)/)[1]) >= 3.1){
          // Mozilla 3.1 or higher
          return true;
        }
      }
      catch(err) {
        // if there's an error, just keep going, we'll assume that drop shadows are not supported
      }

      return false;

    } // </ shadowSupport() >

    /**
     * Given two points, find a point which is dist pixels from point1 on a line to point2
     */
    function betweenPoint(point1, point2, dist) {
      // figure out if we're horizontal or vertical
      var y, x;
      if (point1.x == point2.x) {
        // vertical
        y = point1.y < point2.y ? point1.y + dist : point1.y - dist;
        return {x: point1.x, y: y};
      }
      else if (point1.y == point2.y) {
        // horizontal
        x = point1.x < point2.x ? point1.x + dist : point1.x - dist;
        return {x:x, y: point1.y};
      }
    }; // </ betweenPoint() >

    function centerPoint(arcStart, corner, arcEnd) {
      var x = corner.x == arcStart.x ? arcEnd.x : arcStart.x;
      var y = corner.y == arcStart.y ? arcEnd.y : arcStart.y;
      var startAngle, endAngle;
      if (arcStart.x < arcEnd.x) {
        if (arcStart.y > arcEnd.y) {
          // arc is on upper left
          startAngle = (Math.PI/180)*180;
          endAngle = (Math.PI/180)*90;
        }
        else {
          // arc is on upper right
          startAngle = (Math.PI/180)*90;
          endAngle = 0;
        }
      }
      else {
        if (arcStart.y > arcEnd.y) {
          // arc is on lower left
          startAngle = (Math.PI/180)*270;
          endAngle = (Math.PI/180)*180;
        }
        else {
          // arc is on lower right
          startAngle = 0;
          endAngle = (Math.PI/180)*270;
        }
      }
      return {x: x, y: y, type: 'center', startAngle: startAngle, endAngle: endAngle};
    }; // </ centerPoint() >

    /**
     * Find the intersection point of two lines, each defined by two points
     * arguments are x1, y1 and x2, y2 for r1 (line 1) and r2 (line 2)
     * It's like an algebra party!!!
     */
    function findIntersect(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2) {

      if (r2x1 == r2x2) {
        return findIntersectY(r1x1, r1y1, r1x2, r1y2, r2x1);
      }
      if (r2y1 == r2y2) {
        return findIntersectX(r1x1, r1y1, r1x2, r1y2, r2y1);
      }

      // m = (y1 - y2) / (x1 - x2)  // <-- how to find the slope
      // y = mx + b                 // the 'classic' linear equation
      // b = y - mx                 // how to find b (the y-intersect)
      // x = (y - b)/m              // how to find x
      var r1m = (r1y1 - r1y2) / (r1x1 - r1x2);
      var r1b = r1y1 - (r1m * r1x1);
      var r2m = (r2y1 - r2y2) / (r2x1 - r2x2);
      var r2b = r2y1 - (r2m * r2x1);

      var x = (r2b - r1b) / (r1m - r2m);
      var y = r1m * x + r1b;

      return {x: x, y: y};
    }; // </ findIntersect() >

    /**
     * Find the y intersection point of a line and given x vertical
     */
    function findIntersectY(r1x1, r1y1, r1x2, r1y2, x) {
      if (r1y1 == r1y2) {
        return {x: x, y: r1y1};
      }
      var r1m = (r1y1 - r1y2) / (r1x1 - r1x2);
      var r1b = r1y1 - (r1m * r1x1);

      var y = r1m * x + r1b;

      return {x: x, y: y};
    }; // </ findIntersectY() >

    /**
     * Find the x intersection point of a line and given y horizontal
     */
    function findIntersectX(r1x1, r1y1, r1x2, r1y2, y) {
      if (r1x1 == r1x2) {
        return {x: r1x1, y: y};
      }
      var r1m = (r1y1 - r1y2) / (r1x1 - r1x2);
      var r1b = r1y1 - (r1m * r1x1);

      // y = mx + b     // your old friend, linear equation
      // x = (y - b)/m  // linear equation solved for x
      var x = (y - r1b) / r1m;

      return {x: x, y: y};

    }; // </ findIntersectX() >

  }; // </ jQuery.fn.bt() >

  /**
   * jQuery's compat.js (used in Drupal's jQuery upgrade module, overrides the $().position() function
   *  this is a copy of that function to allow the plugin to work when compat.js is present
   *  once compat.js is fixed to not override existing functions, this function can be removed
   *  and .btPosion() can be replaced with .position() above...
   */
  jQuery.fn.btPosition = function() {

    function num(elem, prop) {
      return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
    };

    var left = 0, top = 0, results;

    if ( this[0] ) {
      // Get *real* offsetParent
      var offsetParent = this.offsetParent(),

      // Get correct offsets
      offset       = this.offset(),
      parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();

      // Subtract element margins
      // note: when an element has margin: auto the offsetLeft and marginLeft
      // are the same in Safari causing offset.left to incorrectly be 0
      offset.top  -= num( this, 'marginTop' );
      offset.left -= num( this, 'marginLeft' );

      // Add offsetParent borders
      parentOffset.top  += num( offsetParent, 'borderTopWidth' );
      parentOffset.left += num( offsetParent, 'borderLeftWidth' );

      // Subtract the two offsets
      results = {
        top:  offset.top  - parentOffset.top,
        left: offset.left - parentOffset.left
      };
    }

    return results;
  }; // </ jQuery.fn.btPosition() >


  /**
  * jQuery's dimensions.js overrides the $().btOuterWidth() function
  *  this is a copy of original jQuery's outerWidth() function to
  *  allow the plugin to work when dimensions.js is present
  */
  jQuery.fn.btOuterWidth = function(margin) {

      function num(elem, prop) {
          return elem[0] && parseInt(jQuery.curCSS(elem[0], prop, true), 10) || 0;
      };

      return this["innerWidth"]()
      + num(this, "borderLeftWidth")
      + num(this, "borderRightWidth")
      + (margin ? num(this, "marginLeft")
      + num(this, "marginRight") : 0);

  }; // </ jQuery.fn.btOuterWidth() >

  /**
   * A convenience function to run btOn() (if available)
   * for each selected item
   */
  jQuery.fn.btOn = function() {
    return this.each(function(index){
      if (jQuery.isFunction(this.btOn)) {
        this.btOn();
      }
    });
  }; // </ $().btOn() >

  /**
   *
   * A convenience function to run btOff() (if available)
   * for each selected item
   */
  jQuery.fn.btOff = function() {
    return this.each(function(index){
      if (jQuery.isFunction(this.btOff)) {
        this.btOff();
      }
    });
  }; // </ $().btOff() >

  jQuery.bt.vars = {clickAnywhereStack: [], closeWhenOpenStack: []};

  /**
   * This function gets bound to the document's click event
   * It turns off all of the tips in the click-anywhere-to-close stack
   */
  jQuery.bt.docClick = function(e) {
    if (!e) {
      var e = window.event;
    };
    // if clicked element is a child of neither a tip NOR a target
    // and there are tips in the stack
    if (!$(e.target).parents().andSelf().filter('.bt-wrapper, .bt-active').length && jQuery.bt.vars.clickAnywhereStack.length) {
      // if clicked element isn't inside tip, close tips in stack
      $(jQuery.bt.vars.clickAnywhereStack).btOff();
      $(document).unbind('click', jQuery.bt.docClick);
    }
  }; // </ docClick() >

  /**
   * Defaults for the beauty tips
   *
   * Note this is a variable definition and not a function. So defaults can be
   * written for an entire page by simply redefining attributes like so:
   *
   *   jQuery.bt.options.width = 400;
   *
   * Be sure to use *jQuery.bt.options* and not jQuery.bt.defaults when overriding
   *
   * This would make all Beauty Tips boxes 400px wide.
   *
   * Each of these options may also be overridden during
   *
   * Can be overriden globally or at time of call.
   *
   */
  jQuery.bt.defaults = {
    trigger:         'hover',                // trigger to show/hide tip
                                             // use [on, off] to define separate on/off triggers
                                             // also use space character to allow multiple  to trigger
                                             // examples:
                                             //   ['focus', 'blur'] // focus displays, blur hides
                                             //   'dblclick'        // dblclick toggles on/off
                                             //   ['focus mouseover', 'blur mouseout'] // multiple triggers
                                             //   'now'             // shows/hides tip without event
                                             //   'none'            // use $('#selector').btOn(); and ...btOff();
                                             //   'hoverIntent'     // hover using hoverIntent plugin (settings below)
                                             // note:
                                             //   hoverIntent becomes default if available

    clickAnywhereToClose: true,              // clicking anywhere outside of the tip will close it
    closeWhenOthersOpen: false,              // tip will be closed before another opens - stop >= 2 tips being on

    shrinkToFit:      false,                 // should short single-line content get a narrower balloon?
    width:            '200px',               // width of tooltip box

    padding:          '10px',                // padding for content (get more fine grained with cssStyles)
    spikeGirth:       10,                    // width of spike
    spikeLength:      15,                    // length of spike
    overlap:          0,                     // spike overlap (px) onto target (can cause problems with 'hover' trigger)
    overlay:          false,                 // display overlay on target (use CSS to style) -- BUGGY!
    killTitle:        true,                  // kill title tags to avoid double tooltips

    textzIndex:       9999,                  // z-index for the text
    boxzIndex:        9998,                  // z-index for the "talk" box (should always be less than textzIndex)
    wrapperzIndex:    9997,
    offsetParent:     null,                  // DOM node to append the tooltip into.
                                             // Must be positioned relative or absolute. Can be selector or object
    positions:        ['most'],              // preference of positions for tip (will use first with available space)
                                             // possible values 'top', 'bottom', 'left', 'right' as an array in order of
                                             // preference. Last value will be used if others don't have enough space.
                                             // or use 'most' to use the area with the most space
    fill:             "rgb(255, 255, 102)",  // fill color for the tooltip box, you can use any CSS-style color definition method
                                             // http://www.w3.org/TR/css3-color/#numerical - not all methods have been tested

    windowMargin:     10,                    // space (px) to leave between text box and browser edge

    strokeWidth:      1,                     // width of stroke around box, **set to 0 for no stroke**
    strokeStyle:      "#000",                // color/alpha of stroke

    cornerRadius:     5,                     // radius of corners (px), set to 0 for square corners

                      // following values are on a scale of 0 to 1 with .5 being centered

    centerPointX:     .5,                    // the spike extends from center of the target edge to this point
    centerPointY:     .5,                    // defined by percentage horizontal (x) and vertical (y)

    shadow:           false,                 // use drop shadow? (only displays in Safari and FF 3.1) - experimental
    shadowOffsetX:    2,                     // shadow offset x (px)
    shadowOffsetY:    2,                     // shadow offset y (px)
    shadowBlur:       3,                     // shadow blur (px)
    shadowColor:      "#000",                // shadow color/alpha
    shadowOverlap:   false,                  // when shadows overlap the target element it can cause problem with hovering
                                             // set this to true to overlap or set to a numeric value to define the amount of overlap
    noShadowOpts:     {strokeStyle: '#999'},  // use this to define 'fall-back' options for browsers which don't support drop shadows

    cssClass:         '',                    // CSS class to add to the box wrapper div (of the TIP)
    cssStyles:        {},                    // styles to add the text box
                                             //   example: {fontFamily: 'Georgia, Times, serif', fontWeight: 'bold'}

    activeClass:      'bt-active',           // class added to TARGET element when its BeautyTip is active

    contentSelector:  "$(this).attr('title')", // if there is no content argument, use this selector to retrieve the title
                                             // a function which returns the content may also be passed here

    ajaxPath:         null,                  // if using ajax request for content, this contains url and (opt) selector
                                             // this will override content and contentSelector
                                             // examples (see jQuery load() function):
                                             //   '/demo.html'
                                             //   '/help/ajax/snip'
                                             //   '/help/existing/full div#content'

                                             // ajaxPath can also be defined as an array
                                             // in which case, the first value will be parsed as a jQuery selector
                                             // the result of which will be used as the ajaxPath
                                             // the second (optional) value is the content selector as above
                                             // examples:
                                             //    ["$(this).attr('href')", 'div#content']
                                             //    ["$(this).parents('.wrapper').find('.title').attr('href')"]
                                             //    ["$('#some-element').val()"]

    ajaxError:        '<strong>ERROR:</strong> <em>%error</em>',
                                             // error text, use "%error" to insert error from server
    ajaxLoading:     '<blink>Loading...</blink>',  // yes folks, it's the blink tag!
    ajaxData:         {},                    // key/value pairs
    ajaxType:         'GET',                 // 'GET' or 'POST'
    ajaxCache:        true,                  // cache ajax results and do not send request to same url multiple times
    ajaxOpts:         {},                    // any other ajax options - timeout, passwords, processing functions, etc...
                                             // see http://docs.jquery.com/Ajax/jQuery.ajax#options

    preBuild:         function(){},          // function to run before popup is built
    preShow:          function(box){},       // function to run before popup is displayed
    showTip:          function(box){
                        $(box).show();
                      },
    postShow:         function(box){},       // function to run after popup is built and displayed

    preHide:          function(box){},       // function to run before popup is removed
    hideTip:          function(box, callback) {
                        $(box).hide();
                        callback();   // you MUST call "callback" at the end of your animations
                      },
    postHide:         function(){},          // function to run after popup is removed

    hoverIntentOpts:  {                          // options for hoverIntent (if installed)
                        interval: 300,           // http://cherne.net/brian/resources/jquery.hoverIntent.html
                        timeout: 500
                      }

  }; // </ jQuery.bt.defaults >

  jQuery.bt.options = {};

})(jQuery);

// @todo
// use larger canvas (extend to edge of page when windowMargin is active)
// add options to shift position of tip vert/horiz and position of spike tip
// create drawn (canvas) shadows
// use overlay to allow overlap with hover
// experiment with making tooltip a subelement of the target
// handle non-canvas-capable browsers elegantly

;window.dealExpired = function (threadid, postid, controltype, securitytoken)
{
    var params = {
        "threadid": threadid,
        "postid": postid,
        "controltype": controltype,
        "ajax": 1,
        "token": securitytoken,
    };

        $.post("/ajax/expired_deals_ajax.php", params, function(response)
        {
            if (response.error)
            {
                errordialog(response.error);
            }
            else if ($(response).find('dologin').length == 1)
            {
                var action_source = 'Report Expired';

                if (controltype == 'deal_feedback_modal')
                {
                    action_source = 'Deal Feedback Modal - Report Expired';

                    sd_ajax_login({action_source: action_source});
                }
                else
                {
                    sd_ajax_login({action_source: action_source});
                }
            }
            else if (response.success)
            {
                if (controltype === 'ddpr')
                {
                    let reportBtn = document.querySelector('[data-role="reportExpired"]');
                    reportBtn.classList.remove('description_button--pink');
                    reportBtn.setAttribute('disabled', true);
                }

                current_counter = parseInt($('#expired_count').attr("currentCount")) + 1;
                $('#expired_count').text(current_counter);
                $('.expired_deal').html('<span class="reportedExpired">Reported as Expired</span>');
                $(".expired_text").text("Reported as Expired");

                if ($('#exposedReportLinkContainer').length)
                {
                    $('#exposedReportLinkContainer').text('Thank you! A staff member will check this deal soon.');
                }
            }
        });
};

window.dealExpiredDialog = function (threadid, postid, controltype, securitytoken)
{
    if (typeof controltype === "undefined" || controltype === null) {
        controltype = "modern";
    }
    var expdialog = $("#expired_dialog");
    if (expdialog.length == 0)
    {
        $("body").append("<div id='expired_dialog' title='Report this deal as expired?'></div>");
        expdialog = $("#expired_dialog");
        expdialog.dialog({
            autoOpen: false,
            modal: true,
            zIndex: 30001,
            width: 300,
            buttons: {
                'Report Expired': function ()
                {
                    dealExpired(threadid, postid, controltype, securitytoken);
                    $(this).dialog('close');
                },
                Cancel: function() {
                    $(this).dialog('close');
                }
            }
        });
    }

    var msg = 'Are you sure you want to report this deal as expired? This will send the deal to our moderators for review.<br /><br />If you\'d like to report this thread for another reason (spam, repost, etc.) please use the <a href="/forums/report.php?p=' + postid + '">Mod Alert</a> form.';
    expdialog.html(msg);
    expdialog.dialog("open");
    $(".ui-dialog-buttonpane button:last").focus();
};

;window.SD_EventTracker = {

    platforms: {},
    eventTypes: {},
    eventCalls: {},
    logging: false,
    enabled: false,
    registerEvents: function (data)
    {
        this.platforms  = data.platforms;
        this.eventTypes = data.eventTypes;
        this.eventCalls = data.eventCalls;
        this.logging    = data.logging;
        this.enabled    = data.enabled;

        this.initCustomEvents();
    },

    trackEvent: function (eventType)
    {
        for (var platform in this.platforms)
        {
            if (typeof this.platforms[platform] != "undefined" &&
                typeof this.eventCalls[platform] != "undefined" &&
                typeof this.eventTypes[platform][eventType] != "undefined")
            {
                params = this.eventTypes[platform][eventType];
                argsStr = this.eventCalls[platform].args;

                //dynamically replace args PARAM_X strings with the param array from this event
                for (var i in params)
                {
                    var strFind = "%%%PARAM_" + (parseInt(i)+1) + "%%%";
                    argsStr = argsStr.replace(strFind, params[i]);
                }
                var argsObj = $.parseJSON(argsStr);
                if (window[this.eventCalls[platform].className] &&
                    window[this.eventCalls[platform].className][this.eventCalls[platform].funcName] &&
                    typeof window[this.eventCalls[platform].className][this.eventCalls[platform].funcName] == "function")
                {
                    window[this.eventCalls[platform].className][this.eventCalls[platform].funcName](argsObj);
                }

                if (typeof console.log == "function" && this.logging)
                {
                    var eventStr = this.eventCalls[platform].className + "." + this.eventCalls[platform].funcName + "(" + argsStr + ")";
                    console.log("SD_EventTracker Fired Event: " + eventType + " => " + eventStr);
                }
            }
        }
    },

    initCustomEvents: function ()
    {
        if (typeof is_landingpage != "undefined" && is_landingpage)
        {
            $(document).on('mousedown', 'a:not(.coupon_title,.coupon_box), span:not([href!=""])', function(e) {
                if (typeof $(this).attr('href') == 'string')
                {
                    var href = $(this).attr('href');

                    if (href.substring(0,2) == '/?' || href.substring(0,2) == '//')
                    {
                        //external links
                        SD_EventTracker.trackEvent('sem_outclick');
                    }
                    else
                    {
                        //internal links
                        SD_EventTracker.trackEvent('sem_click');
                    }
                }
            });
        }
    }
};

;/*!
 * jQuery Cookie Plugin v1.4.1
 * https://github.com/carhartl/jquery-cookie
 *
 * Copyright 2006, 2014 Klaus Hartl
 * Released under the MIT license
 */
(function (factory) {
	if (typeof define === 'function' && define.amd) {
		// AMD
		define(['jquery'], factory);
	} else if (typeof exports === 'object') {
		// CommonJS
		factory(require('jquery'));
	} else {
		// Browser globals
		factory(jQuery);
	}
}(function ($) {

	var pluses = /\+/g;

	function encode(s) {
		return config.raw ? s : encodeURIComponent(s);
	}

	function decode(s) {
		return config.raw ? s : decodeURIComponent(s);
	}

	function stringifyCookieValue(value) {
		return encode(config.json ? JSON.stringify(value) : String(value));
	}

	function parseCookieValue(s) {
		if (s.indexOf('"') === 0) {
			// This is a quoted cookie as according to RFC2068, unescape...
			s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
		}

		try {
			// Replace server-side written pluses with spaces.
			// If we can't decode the cookie, ignore it, it's unusable.
			// If we can't parse the cookie, ignore it, it's unusable.
			s = decodeURIComponent(s.replace(pluses, ' '));
			return config.json ? JSON.parse(s) : s;
		} catch(e) {}
	}

	function read(s, converter) {
		var value = config.raw ? s : parseCookieValue(s);
		return $.isFunction(converter) ? converter(value) : value;
	}

	var config = $.cookie = function (key, value, options) {

		// Write

		if (arguments.length > 1 && !$.isFunction(value)) {
			options = $.extend({}, config.defaults, options);

			if (typeof options.expires === 'number') {
				var days = options.expires, t = options.expires = new Date();
				t.setTime(+t + days * 864e+5);
			}

			return (document.cookie = [
				encode(key), '=', stringifyCookieValue(value),
				options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
				options.path    ? '; path=' + options.path : '',
				options.domain  ? '; domain=' + options.domain : '',
				options.secure  ? '; secure' : ''
			].join(''));
		}

		// Read

		var result = key ? undefined : {};

		// To prevent the for loop in the first place assign an empty array
		// in case there are no cookies at all. Also prevents odd result when
		// calling $.cookie().
		var cookies = document.cookie ? document.cookie.split('; ') : [];

		for (var i = 0, l = cookies.length; i < l; i++) {
			var parts = cookies[i].split('=');
			var name = decode(parts.shift());
			var cookie = parts.join('=');

			if (key && key === name) {
				// If second argument (value) is a function it's a converter...
				result = read(cookie, value);
				break;
			}

			// Prevent storing a cookie that we couldn't decode.
			if (!key && (cookie = read(cookie)) !== undefined) {
				result[name] = cookie;
			}
		}

		return result;
	};

	config.defaults = {};

	$.removeCookie = function (key, options) {
		if ($.cookie(key) === undefined) {
			return false;
		}

		// Must not alter options, thus extending a fresh object...
		$.cookie(key, '', $.extend({}, options, { expires: -1 }));
		return !$.cookie(key);
	};

}));

;/*
 *  NOTE: This file has been manually shimmed with a define factory for compatibility with RequireJS. If this file is replaced the new one must be shimmed also!
 */
(function(factory)
{
    if (typeof define === 'function' && define.amd)
    {
        define('sd.cookie', ['jquery', 'jquery.cookie'], factory);
    }
    else
    {
        factory(jQuery);
    }
}(function($) {
    if (typeof(window.SD) === "undefined")
    {
        window.SD = {};
    }
    (function (SD)
    {
        var decodeCombinedCookie = function (cookieData)
        {
            if (!cookieData)
            {
                return {};
            }
            var keyValuePairs = cookieData.split("&");

            var cookieMap = {};
            $.each(keyValuePairs, function (index, keyValuePair)
            {
                var arr = keyValuePair.split("=");
                cookieMap[arr[0]] = decodeURIComponent(arr[1]);
            });

            return cookieMap;
        };

        var encodeCombinedCookie = function (cookieMap)
        {
            var keyValuePairs = [];
            $.each(cookieMap, function (key, value)
            {
                keyValuePairs.push(key + "=" + encodeURIComponent(value));
            });
            return keyValuePairs.join("&");
        };

        var CombinedCookie = function (containerName, currentCookieData, cookieOptions, cookieDefinitions)
        {
            this.containerName = containerName;
            this.decodedCookie = decodeCombinedCookie(currentCookieData);
            this.cookieOptions = cookieOptions;
            this.cookieDefinitions = cookieDefinitions || {};
        };

        CombinedCookie.prototype.getCookie = function (cookieName)
        {
            var value = this.decodedCookie[cookieName];
            if (this.cookieDefinitions[cookieName])
            {
                value = this.cookieDefinitions[cookieName].deserialize(value);
            }
            return value;
        };

        CombinedCookie.prototype.setCookie = function (cookieName, value)
        {
            if (value === null)
            {
                delete this.decodedCookie[cookieName];
            }
            else
            {
                if (this.cookieDefinitions[cookieName])
                {
                    value = this.cookieDefinitions[cookieName].serialize(value);
                }
                this.decodedCookie[cookieName] = value;
            }
            var oldRaw = $.cookie.raw;
            var oldJson = $.cookie.json;
            $.cookie.raw = true;
            $.cookie.json = false;
            $.cookie(this.containerName, encodeCombinedCookie(this.decodedCookie), this.cookieOptions);
            $.cookie.raw = oldRaw;
            $.cookie.json = oldJson;
        };

        var oldRaw = $.cookie.raw;
        var oldJson = $.cookie.json;
        $.cookie.raw = true;
        $.cookie.json = false;

        var callQueue = [];
        if (SD.CookieService && SD.CookieService.callQueue)
        {
            callQueue = SD.CookieService.callQueue;
        }

        const topLevelDomain = window.location.hostname.split('.').slice(-2).join('.');
        SD.CookieService = {
            persistent: new CombinedCookie('sd_p', $.cookie("sd_p"), {
                expires: 365,
                path: '/',
                domain: "." + topLevelDomain
            }),
            session: new CombinedCookie(
                    'sd_s',
                    $.cookie("sd_s"),
                    {
                        path: '/',
                        domain: "." + topLevelDomain
                    },
                    {
                        funnel: (function ()
                        {
                            var FunnelTypes = function (cookieObj)
                            {
                                var funnelData = {};
                                $.each(cookieObj, function (key, value)
                                {
                                    var state = value.substr(0, 1);
                                    var entryData = value.substr(1).split(',');
                                    var entryPoint = decodeURIComponent(entryData.shift());
                                    var startPage = entryData.join(',');
                                    funnelData[key] = {state: state, entryPoint: entryPoint, startPage: startPage};
                                });

                                this.funnelData = funnelData;
                            };
                            FunnelTypes.prototype.getType = function (type)
                            {
                                return this.funnelData[type];
                            };
                            FunnelTypes.prototype.setType = function (type, state, entryPoint, startPage)
                            {
                                this.funnelData[type] = {state: state, entryPoint: entryPoint, startPage: startPage};
                            };
                            FunnelTypes.prototype.getSerializedType = function (type)
                            {
                                var typeData = this.funnelData[type];
                                return typeData.state + typeData.entryPoint.replace(/[,%]/g, encodeURIComponent) + ',' + typeData.startPage;
                            };
                            return {
                                serialize: function (json)
                                {
                                    var serializedArray = [];
                                    $.each(json.funnelData, function (k, v)
                                    {
                                        var value = v.state + v.entryPoint;
                                        serializedArray.push(
                                                k.replace(/[&=;%]/g, encodeURIComponent)
                                                + '='
                                                + json.getSerializedType(k).replace(/[&;%]/g, encodeURIComponent)
                                        );
                                    });

                                    return serializedArray.join('&');
                                },
                                deserialize: function (serialized)
                                {
                                    if (!serialized)
                                    {
                                        return new FunnelTypes({});
                                    }
                                    var json = {}, serializedArray = serialized.split('&');
                                    $.each(serializedArray, function (index, keyValuePair)
                                    {
                                        var arr = keyValuePair.split("=");
                                        json[decodeURIComponent(arr.shift())] = decodeURIComponent(arr.join('='));
                                    });

                                    return new FunnelTypes(json);
                                }
                            };
                        })()
                    }
            )
        };
        $.each(callQueue, function (index, callback)
        {
            callback();
        });
        SD.CookieService.callQueue = {
            push: function (callback)
            {
                callback();
            }
        };
        $.cookie.raw = oldRaw;
        $.cookie.json = oldJson;
    })(window.SD);
}));
;(function (window)
{
    var parseQueryString = function (queryString)
    {
        if (!queryString.length) return {};
        var queryStringKVPairs = queryString.split('&'),
                queryMap = {};


        for (var i = 0; i < queryStringKVPairs.length; i++)
        {
            var kv = queryStringKVPairs[i].split('=');
            queryMap[decodeURIComponent(kv.shift())] = decodeURIComponent(kv.join('='));
        }

        return queryMap;
    };

    var parseGacSessionData = function (cookieString)
    {
        var gaqCookie = cookieString.match(/__utmz=(?:[0-9]+\.)+([^;]+)/);

        if (!gaqCookie) return {};
        var parameters = ("|" + gaqCookie[1]).match(/\|utmc[a-z]{2}=.*?(?=\|utm|$)/g);
        var campaign = {};
        for (var i = 0; i < parameters.length; i++)
        {
            var kv = parameters[i].substr(1).split('=');
            var k = kv.shift();
            var v = decodeURIComponent(kv.join('='));

            if (k == 'utmcsr') campaign.source = v;
            if (k == 'utmccn') campaign.campaign = v;
            if (k == 'utmcmd') campaign.medium = v;
            if (k == 'utmctr') campaign.term = v;
            if (k == 'utmcct') campaign.content = v;
        }

        return campaign;
    };

    var hasGacActiveCookie = function (cookieString)
    {
        return !!cookieString.match(/__utmb=[^;]+/);
    };

    var cleanValue = function (v)
    {
        if (typeof(v) == "undefined" || v == null || v == '(not set)' || v == '(direct)' || v == '(none)')
        {
            return '';
        }
        return v;
    };

    var compareCampaigns = function (c1, c2)
    {
        if (cleanValue(c1.source) != cleanValue(c2.source)) return false;
        if (cleanValue(c1.campaign) != cleanValue(c2.campaign)) return false;
        if (cleanValue(c1.medium) != cleanValue(c2.medium)) return false;
        if (cleanValue(c1.term) != cleanValue(c2.term)) return false;
        return cleanValue(c1.content) == cleanValue(c2.content);
    };

    var jct = function (queryMap, previousCampaign, referrerHost, referrerPath, currentHost, isNew)
    {
        this.queryMap = queryMap;
        this.previousCampaign = previousCampaign;
        this.referrerHost = referrerHost;
        this.referrerPath = referrerPath;
        this.currentHost = currentHost;
        this.overrideCampaign = {};
        this.isNew = isNew;
    };

    jct.prototype.getCampaignData = function ()
    {
        var campaign = {};

        if (this.queryMap.gclid || this.queryMap.gclsrc)
        {
            campaign.source = 'google';
            campaign.medium = 'cpc';
            campaign.campaign = this.queryMap.utm_campaign;
            campaign.term = this.queryMap.utm_term;
            campaign.content = this.queryMap.utm_content;
        }
        else if (this.overrideCampaign.source)
        {
            campaign.source = this.overrideCampaign.source;
            campaign.medium = this.overrideCampaign.medium || '(not set)';
            campaign.campaign = this.overrideCampaign.campaign || '(not set)';
            campaign.term = this.overrideCampaign.term;
            campaign.content = this.overrideCampaign.content;
        }
        else if (this.queryMap.utm_source)
        {
            campaign.source = this.queryMap.utm_source;
            campaign.medium = this.queryMap.utm_medium || '(not set)';
            campaign.campaign = this.queryMap.utm_campaign || '(not set)';
            campaign.term = this.queryMap.utm_term;
            campaign.content = this.queryMap.utm_content;
        }
        else
        {
            if (this.referrerHost && this.referrerHost != this.currentHost)
            {
                campaign.source = this.referrerHost;
                campaign.medium = 'referral';
                campaign.campaign = '(referral)';
                campaign.content = this.referrerPath;
            }
            else
            {
                campaign.source = this.previousCampaign.source || '(direct)';
                campaign.medium = this.previousCampaign.medium || '(not set)';
                campaign.campaign = this.previousCampaign.campaign || '(not set)';
                campaign.term = this.previousCampaign.term;
                campaign.content = this.previousCampaign.content;
            }
        }
        campaign.isNew = this.isNew || !compareCampaigns(campaign, this.previousCampaign);
        return campaign;
    };

    jct.track = function (campaign)
    {
        var referrerData = document.referrer.match(/^https?:\/\/([^/]+)(\/.*)$/);
        var rHost = referrerData ? referrerData[1] : null;
        var rPath = referrerData ? referrerData[2] : null;

        var tracker = new jct(parseQueryString(window.location.search.substr(1)), parseGacSessionData(window.document.cookie), rHost, rPath, window.location.hostname, !hasGacActiveCookie(window.document.cookie));
        if (campaign) tracker.overrideCampaign = campaign;
        return tracker.getCampaignData();
    };

    window.jct = jct;
})(window);
;/* eslint-disable */
if (typeof(window.SD) === "undefined")
{
    window.SD = {};
}

if (typeof(window.dataLayer) === "undefined")
{
    window.dataLayer = {};
}

SD.Analytics = {
    socialFollow: function (socialNetwork)
    {
        "use strict";

        if (typeof(dataLayer.social) === "undefined")
        {
            dataLayer.social = {
                platform: ""
            };
        }

        if (socialNetwork !== "")
        {
            dataLayer.social.platform = socialNetwork;
        }

        return true;
    },
    socialShare: function (shareType)
    {
        "use strict";

        if (typeof(dataLayer.social) === "undefined")
        {
            dataLayer.social = {
                platform: ""
            };
        }

        if (shareType !== "")
        {
            dataLayer.social.platform = shareType;
        }

        return true;
    },
    loginTrack: function(trackType)
    {
        return false;
    },
    signUpTrack: function(trackType)
    {
        return false;
    },
    newsletterTrack: function(trackType)
    {
        return false;
    },
    dtmTrackRegLogin: function(event, actionSource)
    {
        if (typeof window.gtmDl === 'undefined' || !('registrationTracking' in window.dataLayer))
        {
            return;
        }

        /**
         * We expect a few things:
         * - this is being used primarily for registration, login, newsletter signups, or social registration
         * - previous events should have populated dataLayer.registrationTracking either via:
         *   - regLogController population
         *   - server-side rehydration (if applicable still)
         * - the actual gtm event is fired. No voodoo
         */
        const formType = window.dataLayer.registrationTracking.type.split('|');
        const flow = formType[1].charAt(0).toUpperCase() + formType[1].slice(1);
        const eventData = {
            'event': event,
            'js rl action source': actionSource,
            'js rl category': flow,
            'js rl form experience': formType[0].charAt(0).toUpperCase() + formType[0].slice(1),
            'js rl form flow': flow,
            'js rl label': window.dataLayer.registrationTracking.method,
        };

        if ('newsletterSubscriptionId' in window.dataLayer.registrationTracking)
        {
            eventData['js rl newsletter subscription id'] = window.dataLayer.newsletterSubscriptionId;
        }

        window.gtmDl.push(eventData);
    },
    sendOutclick: function (outclickData)
    {
        window.dataLayer.product = window.dataLayer.product || {};
        window.dataLayer.outclick = {};
        dataLayer.product.products = outclickData.productId ? ';' + outclickData.productId : '';
        dataLayer.product.forum = outclickData.forum;
        dataLayer.product.exitWebsite = outclickData.exitWebsite;
        dataLayer.outclick.typeOfOutclick = outclickData.typeOfOutclick;
        dataLayer.outclick.identifier = outclickData.adobeRef;
        dataLayer.outclick.clickType = outclickData.clickType;
        if (typeof outclickData.query === 'object')
        {
            if ('sdtrk' in outclickData.query)
            {
                dataLayer.outclick.sdTrack = outclickData.query.sdtrk;
            }
            if ('attr_track' in outclickData.query)
            {
                dataLayer.outclick.attrTrack = '';
                if (outclickData.query.attr_track.length > 100)
                {
                    var lastPipe = outclickData.query.attr_track.lastIndexOf('|', 100);
                    dataLayer.outclick.attrTrack = outclickData.query.attr_track.substr(0, lastPipe);
                }
                else
                {
                    dataLayer.outclick.attrTrack = outclickData.query.attr_track;
                }
            }
        }
    },
    injectSDTrk: function (url, sdtrk)
    {
        var delim, uSplode = new RegExp("u(\\d+)=");

        sdtrk = 'sdtrk=' + sdtrk;

        if (uSplode.test(url))
        {
            delim = uSplode.exec(url);
            delim = ["u", delim[1], "="].join("");

            url = url.split(delim);
            url[0] = url[0] + sdtrk + "&";
            url = url.join(delim);
        }
        else
        {
            url = url.indexOf("?") > 0 ?
                (url + "&" + sdtrk) :
                (url + "?" + sdtrk);
        }

        return url;
    },
    trackActivityMap: function (link, region)
    {
        window.s_objectID = link;
        window.s_regionID = region;
    },
    'trackTestEvent': function (category, label)
    {
        category = category.replace('|', '');
        label = label.replace('|', '');

        dataLayer.abTestEventDataDirect = category + '|' + label;
    },
    addDataLayerProductTitle: function(title)
    {
        if (typeof(dataLayer.product) === "undefined")
        {
            dataLayer.product = {
                title: title
            };
        }
        else
        {
            dataLayer.product.title = title;
        }
    },
    addDataLayerProductId: function(threadId)
    {
        if (typeof(dataLayer.product) === "undefined")
        {
            dataLayer.product = {
                products: ';' + threadId
            };
        }
        else
        {
            dataLayer.product.products = ';' + threadId;
        }
    },
    addDataLayerCouponIdentifier: function(couponId)
    {
        "use strict";

        if (typeof(dataLayer.coupon) === "undefined")
        {
            dataLayer.coupon = {
                identifier: couponId
            };
        }
        else
        {
            dataLayer.coupon.identifier = couponId;
        }
    },
    ajaxThreadTracking: function(threadids)
    {
        var threadString = "";
        for (var i = 0; i < threadids.length; i++)
        {
            threadString += ";" + threadids[i] + ","
        }
        threadString = threadString.substring(0, (threadString.length - 1));
        dataLayer.product.products = threadString;
    },
    addDataLayerProductTileNumber: function(tilePosition)
    {
        if (typeof(dataLayer.product) === "undefined")
        {
            dataLayer.product = {
                tileNumber: tilePosition
            };
        }
        else
        {
            dataLayer.product.tileNumber = tilePosition;
        }
    },
    addDataLayerProductFireSign: function(fireSignEnabled)
    {
        var fireSignOn = fireSignEnabled ? 'yes' : 'no';

        if (typeof(dataLayer.product) === "undefined")
        {
            dataLayer.product = {
                fireSign: fireSignOn
            };
        }
        else
        {
            dataLayer.product.fireSign = fireSignOn;
        }
    },
    addDataLayerProductModule: function(moduleName)
    {
        if (typeof(dataLayer.product) === "undefined")
        {
            dataLayer.product = {
                module: moduleName
            };
        }
        else
        {
            dataLayer.product.module = moduleName;
        }
    },
    trackDealClick: function(threadId, position, fireSign, module)
    {
        SD.Analytics.addDataLayerProductId(threadId);
        SD.Analytics.addDataLayerProductTileNumber(position);
        SD.Analytics.addDataLayerProductFireSign(fireSign);
        SD.Analytics.addDataLayerProductModule(module);
    },
    getGridTilePosition: function(tileIndex)
    {
        var windowWidth =  $(window).width();
        var tilesPerRow = 7;

        if (windowWidth < 1099)
        {
            tilesPerRow = 4;
        }
        else if (windowWidth < 1372)
        {
            tilesPerRow = 5;
        }
        else if (windowWidth < 1540)
        {
            tilesPerRow = 6;
        }

        var row = Math.ceil(tileIndex / tilesPerRow);
        var col = tileIndex % tilesPerRow == 0 ? tilesPerRow : tileIndex % tilesPerRow;

        return row + ':' + col;
    },
    getClassicGridTilePosition: function(tileIndex)
    {
        var windowWidth =  $(window).width();
        var tilesPerRow = 6;

        if (windowWidth < 1117)
        {
            tilesPerRow = 4;
        }
        else if (windowWidth < 1270)
        {
            tilesPerRow = 5;
        }

        var row = Math.ceil(tileIndex / tilesPerRow);
        var col = tileIndex % tilesPerRow == 0 ? tilesPerRow : tileIndex % tilesPerRow;

        return row + ':' + col;
    },
    getClassicCategoryTilePosition: function(tileIndex)
    {
        var windowWidth =  $(window).width();
        var tilesPerRow = 3;

        if (windowWidth < 1344)
        {
            tilesPerRow = 2;
        }

        var row = Math.ceil(tileIndex / tilesPerRow);
        var col = tileIndex % tilesPerRow == 0 ? tilesPerRow : tileIndex % tilesPerRow;

        return row + ':' + col;
    },
    handleFpDealLink: function(linkElement, view)
    {
        var parentElement, headingElement, position;

        if (view == 'redesign')
        {
            parentElement = linkElement.closest('.fpGridBox');
            headingElement = linkElement.closest('.gridCategory').find('.headingLeft');

            if (parentElement.hasClass('grid'))
            {
                position = SD.Analytics.getGridTilePosition(parentElement.first().index());
            }
            else
            {
                position = parentElement.first().index() + 1 + ':1';
            }
        }
        else if (view == 'classic')
        {
            parentElement = linkElement.closest('.dealitem');
            headingElement = parentElement.prevAll('.deal_header_date:first');

            if ($("#deal_list").hasClass("grid"))
            {
                position = SD.Analytics.getClassicGridTilePosition(parentElement.data('position'));
            }
            else
            {
                position = parentElement.data('position') + ':1';
            }
        }
        else if (view == 'mobile')
        {
            parentElement = linkElement.closest('.coupon-content');
            headingElement = linkElement.closest('.wrapper').prev().find('.date');
            position = parentElement.data('position') + ':1';
        }

        var threadId = parentElement.data('threadid');
        var fireDeal = parentElement.hasClass('firedeal');
        var heading = headingElement.data('deal-list-heading') || headingElement.text().trim();

        SD.Analytics.trackDealClick(threadId, position, fireDeal, heading);
    },
    handleClassicFpDetailsLink: function(linkElement)
    {
        var parentElement = linkElement.closest('.deal_body');

        var threadId = parentElement.prev().data('threadid');
        var fireDeal = parentElement.prev().hasClass('firedeal');
        var position = parentElement.prev().data('position') + ':1';
        var heading = parentElement.prevAll('.deal_header_date:first').find('.deal_header_date_link').text().trim();

        SD.Analytics.trackDealClick(threadId, position, fireDeal, heading);
    },
    handlePopularDealLink: function(linkElement, view)
    {
        var parentElement, heading;

        if (view == 'mobile')
        {
            parentElement = linkElement.closest('.coupon-content');
            heading = linkElement.closest('.wrapper').prev().find('.date').text().trim();
        }
        else
        {
            parentElement = linkElement.closest('.dealRow');
            heading = $('.popularDealsPageTitle').data('heading');
        }

        var threadId = parentElement.data('threadid');
        var fireDeal = parentElement.hasClass('firedeal');
        var position = parentElement.data('position') + ':1';

        SD.Analytics.trackDealClick(threadId, position, fireDeal, heading);
    },
    handleStoreDealLink: function(linkElement, view)
    {
        var parentElement, position, headingElement;

        if (view == 'classic')
        {
            parentElement = linkElement.closest('.deal_block');
            headingElement = '.fpcontent.store.deals';

            position = SD.Analytics.getClassicCategoryTilePosition(parentElement.data('position'));
        }
        else if (view == 'redesign')
        {
            parentElement = linkElement.closest('.item');
            position = parentElement.data('bestorder') + ':1';
            headingElement = '#storeTop';
        }
        else if (view == 'mobile')
        {
            parentElement = linkElement.closest('.coupon-content');
            position = parentElement.data('position') + ':1';
            headingElement = '#dealWrapper';
        }

        var threadId = parentElement.data('threadid');
        var fireDeal = parentElement.hasClass('firedeal');
        var heading = $(headingElement).data('heading');

        SD.Analytics.trackDealClick(threadId, position, fireDeal, heading);
    },
    addDataLayerFrontpageLayout: function(layoutType)
    {
        if (typeof(dataLayer.frontpage) === "undefined")
        {
            dataLayer.frontpage = {
                layout: layoutType
            };
        }
        else
        {
            dataLayer.frontpage.layout = layoutType;
        }
    },
    updateFormFunnel: function (type, entryPoint, entryPage, status)
    {
        dataLayer.formFunnel = {
            type: type,
            entryPoint: entryPoint,
            status: status,
            startPage: entryPage
        };
    },
    handleClick: function (selector, callback)
    {
        this.clickHandlers.push({selector: selector, callback: callback});
    },
    clickHandlers: [],
    processGlobalClick: function (ev)
    {
        var sendClickEvent = false;
        $.each(SD.Analytics.clickHandlers, function (i, handler)
        {
            var matches = $(ev.target).closest(handler.selector);
            if (matches.length)
            {
                sendClickEvent = handler.callback(matches) || sendClickEvent;
            }
        });
    },
    modalImpressionTrack: function(modalName)
    {
        if (modalName.length === 0)
        {
            return;
        }

        if (!dataLayer.modal)
        {
            dataLayer.modal = {};
        }
        dataLayer.modal.modalName = modalName;
    }
};

window.dataLayer.gaCampaign = jct.track();

window.sdLoadQueue.push([function()
{
    "use strict";

    if (!!window['\u200BsdAnalytics'] === false)
    {
        window['\u200BsdAnalytics'] = true;
    }
    else
    {
        return false;
    }

    //Adobe social sharing analytics tracking
    $('.social-follow').on('click', function()
    {
        SD.Analytics.socialFollow($(this).data('platform'));
        return true;
    });

    $('.social-share').on('click', function()
    {
        SD.Analytics.socialShare($(this).data('platform'));
        return true;
    });

   $(window).on('click', SD.Analytics.processGlobalClick);

    SD.Analytics.handleClick("[data-funnel-entry][data-funnel-type]", function (target)
    {
        SD.Analytics.updateFormFunnel(target.data('funnel-type'), target.data('funnel-entry'), dataLayer.page.pageName, 'started');
        var funnelCookie = SD.CookieService.session.getCookie('funnel');
        funnelCookie.setType(target.data('funnel-type'), 1, target.data('funnel-entry'), dataLayer.page.pageName);
        SD.CookieService.session.setCookie('funnel', funnelCookie);
        return true;
    });

    var formInteracted = false;
    var funnelForms = $("form[data-funnel-type]");
    funnelForms.each(function ()
    {
        var type = $(this).closest("form").data('funnel-type');
        var funnels = SD.CookieService.session.getCookie('funnel');
        var funnelTypeData = funnels.getType(type);
        var entry, statuses;
        if (!funnelTypeData)
        {
            entry = '(none specified)';
            statuses = 'started';

            SD.Analytics.updateFormFunnel(type, entry, entry, statuses);
            funnels.setType(type, 1, entry, entry);
            SD.CookieService.session.setCookie('funnel', funnels);
        }

    });
    funnelForms.find("input,select,textarea").click(function ()
    {
        if (!formInteracted)
        {
            var type = $(this).closest("form").data('funnel-type');
            var funnels = SD.CookieService.session.getCookie('funnel');
            var funnelData = funnels.getType(type);
            var state, entry, startPage, statuses;
            if (funnelData)
            {
                state = funnelData.state;
                entry = funnelData.entryPoint;
                startPage = funnelData.startPage;
                statuses = 'interacted';
            }
            else
            {
                state = 1;
                startPage = entry = '(none specified)';
                statuses = 'interacted';
            }

            if (state == 1)
            {
                SD.Analytics.updateFormFunnel(type, entry, startPage, statuses);
                funnels.setType(type, 2, entry, startPage);
                SD.CookieService.session.setCookie('funnel', funnels);
            }
        }
    });

    // Sign-up, Login, Newsletter Tracking
    if (window.location.href.indexOf('login=1') >= 0)
    {
        SD.Analytics.loginTrack('successful');
    }

    $('.track-sign-up').on('click', function(ev) {
        SD.Analytics.signUpTrack('start');
        return true;
    });

    $('.track-login').on('click', function(ev) {
        SD.Analytics.loginTrack('start');
        return true;
    });

    $('.track-newsletter-checkbox').on('click', function(ev)
    {
        if (this.checked)
        {
            SD.Analytics.newsletterTrack('start');
            return true;
        }
    });

    $('.track-fpDealLink').on('click', function(ev) {
        SD.Analytics.handleFpDealLink($(ev.target), 'redesign');
        return true;
    });

    $(document).on('click', '.track-fpDealDetailLink', function(ev) {
        SD.Analytics.handleFpDealLink($(this), 'redesign');
        return true;
    });

    $(document).on('click', '.track-classicFpDetailsLink', function(ev) {
        SD.Analytics.handleClassicFpDetailsLink($(this));
        return true;
    });

    $('.track-popularDealLink').on('click', function(ev) {
        SD.Analytics.handlePopularDealLink($(this));
        return true;
    });

    $('.track-storeDealLink').on('click', function(ev) {
        SD.Analytics.handleStoreDealLink($(this), 'redesign');
        return true;
    });

    $('.track-classicStoreDealLink').on('click', function(ev) {
        SD.Analytics.handleStoreDealLink($(this), 'classic');
        return true;
    });

    $(document).on('click', '#dealWrapper .coupon-content a', function(ev) {
        SD.Analytics.handleStoreDealLink($(this), 'mobile');
        return true;
    });

    $(document).on('click', '.box.onFrontPage a', function(ev) {
        SD.Analytics.handleFpDealLink($(this), 'mobile');
        return true;
    });

    $(document).on('click', '.box.onPopularDeals a', function(ev) {
        SD.Analytics.handlePopularDealLink($(this), 'mobile');
        return true;
    });
}, 'analytics']);

;/* eslint-disable */
window.sd_outclick_handler = window.sd_outclick_handler || [];
if (typeof(window.SD) === "undefined")
{
    window.SD = {};
}

(function(SD) {
    const moduleReferrerHandler = {
        moduleCookieNames: ['mo_gtm', 'mo_pos'],
        moduleReferrers: {},
        transferCookiesToMemory: function ()
        {
            const cookies = this.getCookies();

            for (let i = 0; i < cookies.length; i++)
            {
                const cookie = cookies[i].split('=');

                if (this.moduleCookieNames.indexOf(cookie[0]) > -1)
                {
                    this.moduleReferrers[cookie[0]] = cookie[1];
                    this.clearCookie(cookie[0]);
                }
            }
        },
        getReferrers: function ()
        {
            // If no referrer is set, try once more in the case where the current outclick
            // is directly from a module, meaning the cookie was set AFTER page load but before
            // the outclick
            if (Object.keys(this.moduleReferrers).length === 0)
            {
                this.transferCookiesToMemory();
            }

            return this.moduleReferrers;
        },
        removeReferrers: function ()
        {
            this.moduleReferrers = {};
        },
        getCookies: function ()
        {
            return document.cookie.split('; ');
        },
        clearCookie: function (cookieName)
        {
            document.cookie = cookieName + '=false; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
        }
    };

    // On page load, determine if the user was referred to the page by a module. If they were,
    // they will have a session cookie (mo_gtm) set to the name of the referring module. This
    // name will be sent to GTM as a module outclick if they outclick from this page
    moduleReferrerHandler.transferCookiesToMemory();

    var outclickRegex = /\/(?:click)?\?(?:|.*?&)(pno|lno|u2|u1|u3|sid|tid|sn|cno|kat|ccid|ddid|bfid)(?:=|&|$)/;
    var hrefIsOutclick = function (target)
    {
        return target.href.match(outclickRegex);
    };
    var listener = function (ev) {
        var target = ev.target;
        var maxDepth = 10;

        while (target && maxDepth--)
        {
            if (target.nodeName == 'A'
                    && (target.oldHref || hrefIsOutclick(target)))
            {
                break;
            }

            target = target.parentNode;
            if (!target || target == document.body || maxDepth <= 0)
            {
                return;
            }
        }

        if (ev.type == 'mousedown')
        {
            var ua = window.navigator.userAgent;
            var isIe = ua.indexOf('MSIE ') > -1 || ua.indexOf('Trident/') > -1 || ua.indexOf('Edge/') > -1;

            if (isIe && (ev.ctrlKey || ev.button == 1) && !target.oldHref)
            {
                target.oldHref = target.href;
                target.href = 'javascript:;';
            }
        }

        if (ev.type == 'click')
        {
            if (target.oldHref)
            {
                target.href = target.oldHref;
                delete target.oldHref;
            }
            if (ev.button == 0)
            {
                if (ev.ctrlKey)
                {
                    ev.clickType = 'Ctrl+Click';
                }
                else if (ev.metaKey)
                {
                    ev.clickType = 'Ctrl+Click';
                }
                else if (ev.shiftKey)
                {
                    ev.clickType = 'Shift+Click';
                }
                else
                {
                    ev.clickType = 'Left Click';
                }
            }
            else if (ev.button == 1)
            {
                ev.clickType = 'Middle Click';
            }
            else if (ev.button == 2)
            {
                //right clicks will be handled by contextmenu
                return;
            }
            else
            {
                ev.clickType = 'Unknown Click';
            }
            callOutclickListenersFromEvent(ev, target);
        }
        else if (ev.type == 'contextmenu')
        {
            if (ev.button == 2)
            {
                ev.clickType = 'Right Click';
                callOutclickListenersFromEvent(ev, target);
            }
        }
    };

    window.addEventListener('mousedown', listener);
    window.addEventListener('click', listener);
    window.addEventListener('contextmenu', listener);

    var outclickListeners = [];

    var callOutclickListenersFromEvent = function (ev, target)
    {
        if (ev.triggeredOutclick || ev.skipOutclick)
        {
            return;
        }
        else
        {
            ev.triggeredOutclick = true;
        }
        callOutclickListeners(target, 'href', target.href, ev.clickType);
    };

    var callOutclickListeners = function (target, attribute, url, clickType)
    {
        var outclickData = buildOutclickData(target, attribute, url, clickType);
        for (var i = 0; i < outclickListeners.length; i++)
        {
            outclickListeners[i](outclickData);
        }
    };

    var buildOutclickData = function (target, attribute, url, clickType)
    {
        var uuid = getAndIncrementSid();

        if (!clickType)
        {
            clickType = 'custom';
        }

        var queryData = {},
            parts = url.split('?');

        if (parts.length > 1)
        {
            var queryString = parts[1];
            var queryParts = queryString.split('&');
            for (var p = queryParts.length - 1; p >= 0; p--)
            {
                var param = queryParts[p].split('=');
                queryData[param[0]] = decodeURIComponent(param[1]);
            }
        }

        return {
            productId: target.getAttribute('data-product-products'),
            forum: target.getAttribute('data-product-forum'),
            exitWebsite: target.getAttribute('data-product-exitwebsite'),
            typeOfOutclick: target.getAttribute('data-outclick-typeofoutclick'),
            adobeRef: uuid,
            clickType: clickType,
            url: url,
            query: queryData,
            target: target,
            attribute: attribute,
            sdtrk: 'sdtrk' in queryData ? queryData.sdtrk : null
        };
    };

    SD.OutclickListener = {
        onOutclick : function (cb)
        {
            outclickListeners.push(cb);
        },
        hitManualOutclick: function (target, attribute, url, clickType)
        {
            if (url.match(outclickRegex))
            {
                if (!clickType)
                {
                    clickType = 'custom';
                }
                callOutclickListeners(target, attribute, url, clickType);
            }
        },
        outclickLinkRewriter: function (outclickData)
        {
            var url = outclickData.url;
            var target = outclickData.target;
            var attribute = outclickData.attribute;
            if (!target || !attribute)
            {
                return;
            }

            var urlParts = url.split('?',2);
            var qs, path = urlParts[0];
            var uuid = outclickData.adobeRef;
            if (urlParts.length === 2)
            {
                qs = url.substring(path.length + 1);
            }
            else
            {
                qs = '';
            }
            qs = qs.replace(/(adobeRef=[^&]*(&|$)|^)/, 'adobeRef=' + uuid + '&');

            let newUrl = path + '?' + qs;

            if (window.sdExtensionPresent === 'Yes')
            {
                try
                {
                    url = new URL(newUrl, window.location.origin);
                    if (url.searchParams.get('exp') !== 'brx')
                    {
                        const originalMst = url.searchParams.get('attr_track') || '';
                        let updatedMst = originalMst;

                        const featureExtensionMst = 'Feature:Extension:';

                        if (updatedMst.indexOf(featureExtensionMst) === -1)
                        {
                            updatedMst = this.addTupleToMst(featureExtensionMst + 'Not Used', updatedMst);
                        }

                        const extensionDistributionMst = 'Extension:Distribution:';

                        if (updatedMst.indexOf(extensionDistributionMst) === -1)
                        {
                            updatedMst = this.addTupleToMst(extensionDistributionMst + (window.sdExtensionDistribution || 'Unknown'), updatedMst);
                        }

                        if (originalMst !== updatedMst)
                        {
                            url.searchParams.set('attr_track', updatedMst);
                            const u2 = url.searchParams.get('u2');
                            if (u2)
                            {
                                url.searchParams.delete('u2');
                                url.searchParams.set('u2', u2);
                            }
                            newUrl = url.toString();
                        }
                    }
                }
                catch(ex)
                {
                    console.log(ex);
                }
            }

            target.setAttribute(attribute, newUrl);
        },
        addTupleToMst(tuple, mst)
        {
            mst = mst || '';

            if (mst)
            {
                mst += '|';
            }

            mst += tuple;

            return mst;
        },
        adobeOutclickHandler: function (outclickData)
        {
            SD.Analytics.sendOutclick(outclickData);
        },
        gtmOutclickHandler: function (outclickData)
        {
            if (typeof gtmDl !== 'undefined')
            {
                outclickData = outclickData || {};

                const gtmOutclickData = {
                    'event': 'sdOutclick',
                    'outclickData': outclickData,
                    'flowSource': outclickData.sdtrk
                };

                gtmDl.push(gtmOutclickData);

                if (moduleReferrerHandler.getReferrers())
                {
                    const referrerData = moduleReferrerHandler.getReferrers();

                    if ('mo_gtm' in referrerData)
                    {
                        const gtmModuleOutclickData = {
                            'event': 'Module Outclick',
                            'moduleName': referrerData['mo_gtm'],
                        };

                        gtmDl.push(gtmModuleOutclickData);
                    }

                    if ('mo_pos' in referrerData && typeof window.gtmDl !== 'undefined')
                    {
                        gtmDl.push({'moduleItemPosition': referrerData['mo_pos']});
                    }

                    moduleReferrerHandler.removeReferrers();
                }
            }
        },
        getOutclickModuleName: function (parentElm) {
            let moduleName = parentElm && parentElm.dataset ? parentElm.dataset.moduleName : undefined;

            if (!moduleName) {
                const referrerData = moduleReferrerHandler.getReferrers();

                moduleName = (referrerData && 'mo_gtm' in referrerData) ? referrerData['mo_gtm'] : undefined;
            }

            return moduleName;
        },
        getOutclickModuleItemPosition: function (clickElm, children) {
            let position = (clickElm && clickElm.tagName && clickElm.tagName === 'BODY') || undefined === children ? undefined : children.indexOf(clickElm) + 1;

            if (!position) {
                const referrerData = moduleReferrerHandler.getReferrers();

                position = (referrerData && 'mo_pos' in referrerData) ? referrerData['mo_pos'] : undefined;
            }

            return position;
        },
        getAdobeRef: function(){
            return getAndIncrementSid();
        }
    };

    var getAndIncrementSid = function()
    {
        "use strict";

        var sid, counter;

        if (!window.hasOwnProperty('\u200BviewSid'))
        {
            return '';
        }

        sid = window['\u200BviewSid'];

        counter = parseInt(sid.substring(32) , 16);
        counter += 1;

        window['\u200BviewSid'] = sid.substring(0, 32) + ('0000' + counter.toString(16)).substr(-4);

        return sid;
    };

    SD.OutclickListener.onOutclick(function (outclickData)
    {
        SD.OutclickListener.outclickLinkRewriter(outclickData);
        SD.OutclickListener.adobeOutclickHandler(outclickData);
        SD.OutclickListener.gtmOutclickHandler(outclickData);
    });

    for (var i = 0; i < sd_outclick_handler.length; i++)
    {
        SD.OutclickListener.onOutclick(sd_outclick_handler[i]);
    }

    sd_outclick_handler.push = function (i) {
        SD.OutclickListener.onOutclick(i);
    };
})(window.SD);