
/*jslint devel: false, browser: true, undef: true, unparam: true, sloppy: true, vars: true, white: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
/*global $, $$, Event, Ajax */

// see http://www.bigbold.com/snippets/posts/show/701
String.prototype.trim = function()
{
    return this.replace(/^\s+|\s+$/g, "");
};

//removes duplicates from a SORTED array
Array.prototype.unique = function(valueFunc)
{
    var i;
    if (this.length < 2) {
        return;
    }

    if (!valueFunc) {
        valueFunc = function(x) {return x;};
    }

    var prev = valueFunc(this[0]);
    for (i = 1; i < this.length; ) {
        var curr = valueFunc(this[i]);
        // (Kevin 7/28/11) note that we need to do a sloppy compare here because we don't know the types returned by valueFunc
        // it may *want* zero to look equal to an empty string
        /*jslint eqeq: true */
        if (prev == curr) {
            this.splice(i, 1);
        } else {
            prev = curr;
            i++;
        }
    }
};

// Ported from http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html.
// NOTE this assumes distinct values.  If there are duplicates, is might not return the first one.
Array.prototype.binarySearch = function(key)
{
    var low = 0;
    var high = this.length - 1;

    while (low <= high) {
        // (Kevin 7/28/11 - allow bitwise operator here)
        /*jslint bitwise: true */
        var mid = (low + high) >>> 1;
        var midVal = this[mid];

        if (midVal < key) {
            low = mid + 1;
        }
        else if (midVal > key) {
            high = mid - 1;
        }
        else {
            return mid; // key found
        }
    }
    return -(low + 1);  // key not found.
};

var Tx = {};
Tx.browser = {};

Tx.isIE = function()
{
    if (Tx.browser.ie === undefined) {
        Tx.browser.ie = navigator.appName.indexOf("Internet Explorer") >= 0;
    }
    return Tx.browser.ie;
};

Tx.isIE6 = function()
{
    if (Tx.browser.ie6 === undefined) {
        // from http://ajaxian.com/archives/detecting-ie7-in-javascript
        Tx.browser.ie6 = typeof document.body.style.maxHeight === "undefined";
    }
    return Tx.browser.ie6;
};

Tx.isIE7 = function()
{
    return Tx.isIE() && !Tx.isIE6();
};

Tx.getInternetExplorerVersion = function()
{
    if (Tx.ieVersion === undefined) {
        // From http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
        //Returns the version of Internet Explorer or a -1
        //(indicating the use of another browser).
        Tx.ieVersion = -1; // Return value assumes failure.
        if (navigator.appName === 'Microsoft Internet Explorer') {
            var ua = navigator.userAgent;
            var re  = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
            if (re.exec(ua)) {
                Tx.ieVersion = parseFloat( RegExp.$1 );
            }
        }
    }
    return Tx.ieVersion;
};

Tx.isTrueIE7 = function()
{
    return Tx.getInternetExplorerVersion() === 7;
};

Tx.isIE8 = function()
{
    return Tx.getInternetExplorerVersion() === 8;
};

Tx.isIE9 = function()
{
    return Tx.getInternetExplorerVersion() === 9;
};

Tx.isSafari = function()
{
    if (Tx.browser.safari === undefined) {
        // Chrome, before version 15, acted like Safari
        var ua = navigator.userAgent;
        var isSafari = ua.indexOf("Safari") >= 0;
        var isChrome = ua.indexOf("Chrome") >= 0;
        if (isChrome) {
            var version = 0;
            var re = new RegExp("Chrome/([0-9]*)");
            if (re.exec(ua)) {
                version = parseFloat(RegExp.$1);
            }
            if (version >= 15) {
                Tx.browser.safari = false;
                Tx.browser.chrome = true;
            }
            else {
                Tx.browser.safari = true;
                Tx.browser.chrome = false;
            }
        }
        else {
            Tx.browser.safari = isSafari;
            Tx.browser.chrome = false;
        }
    }
    return Tx.browser.safari;
};

Tx.isChrome = function()
{
    if (Tx.browser.chrome === undefined) {
        // the calculation for Chrome is in isSafari, since they share a lot of user agent string
        Tx.isSafari();
    }
    return Tx.browser.chrome;
};

Tx.isFirefox = function()
{
    if (Tx.browser.firefox === undefined) {
        Tx.browser.firefox = navigator.userAgent.indexOf("Firefox") >= 0;
    }
    return Tx.browser.firefox;
};

Tx.isIPhone = function()
{
    if (Tx.browser.iPhone === undefined) {
        Tx.browser.iPhone = navigator.userAgent.indexOf("iPhone") >= 0;
    }
    return Tx.browser.iPhone;
};

Tx.isIPad = function()
{
    if (Tx.browser.iPad === undefined) {
        Tx.browser.iPad = navigator.userAgent.indexOf("iPad") >= 0;
    }
    return Tx.browser.iPad;
};

Tx.isAndroid = function()
{
    if (Tx.browser.android === undefined) {
        Tx.browser.android = navigator.userAgent.indexOf("Android") >= 0;
    }
    return Tx.browser.android;
};

Tx.isMobileAndroid = function()
{
    if (Tx.browser.mobileAndroid === undefined) {
        Tx.browser.mobileAndroid = Tx.isAndroid() && navigator.userAgent.indexOf("Mobile") >= 0;
    }
    return Tx.browser.mobileAndroid;
};

Tx.isTouch = function()
{
    return Tx.isIPhone() || Tx.isIPad() || Tx.isAndroid();
};

Tx.isMobile = function()
{
    return Tx.isIPhone() || Tx.isMobileAndroid();
};

// get an internationalized message out of an element
// the id is the DOM id of a span wrapped around one of our tx:Insert spans
Tx.message = function(id)
{
    return $(id).firstChild.nodeValue;
};

// log a message on the server side
// severity can be "error", "warn", "info" or "debug" or can be missing (meaning "warn")
// message is what will be logged
Tx.serverLog = function(severity, message)
{
    var url = "/action/clientLog?message=" + encodeURIComponent(message);
    if (severity) {
        url += "&severity=" + severity;
    }

    var optionsDict = {
            method: 'get'
    };
    // we ignore any response
    Tx.ajax(url, optionsDict);
};

// convenience cover over Ajax.Request
// so we can avoid lint complaints
// and ease any transition to a new js library
Tx.ajax = function(url, options)
{
    // tack a flag onto the url indicating that this is a shopstyle ajax request.  Then we can see the ajax requests in the web logs, etc.
    url = Tx.replaceParam(url, 'ssAjax', '1');
    new Ajax.Request(url, options);
};

// convenience over eval
// both to make sure it is implemented correctly
// and to avoid lint complaints
// Tx.evaluate should be used on a javascript string that is an expression - and we want the value of the expression
Tx.evaluate = function(str)
{
    // ALWAYS NOTE THAT eval IS A SERIOUS SECURITY HAZARD!  Make sure you have complete control over the passed-in string
    // why do we use the quoted parens?  See: http://stackoverflow.com/questions/964397/why-does-javascripts-eval-need-parentheses-to-eval-json-data
    /* (Kevin 7/28/11) the one place in our code that should call eval.  If you need it (really need it), call this method please. */
    /*jslint evil: true */
    return eval('(' + str + ')');
};

//convenience over eval
//both to make sure it is implemented correctly
//and to avoid lint complaints
// Tx.exec should be used on a javascript statement - we want to execute the statement and don't care about any returned value
Tx.exec = function(str)
{
    // note that we do not have parens in quotes around the argument to eval.  That is so that we don't get confused when the string
    // itself contains quotes.  It does mean that we won't necessarily get a return value, but if you want that then call Tx.evaluate.
    // there are also some odd corner cases with labels, so don't use those in the passed-in script
    // ALWAYS NOTE THAT eval IS A SERIOUS SECURITY HAZARD!  Make sure you have complete control over the passed-in string
 /* (Kevin 7/28/11) the one place in our code that should call eval.  If you need it (really need it), call this method please. */
 /*jslint evil: true */
    eval(str);
};

/*
 * Client side handler for DA ajax requests which update the dom.
 * Application must include prototype.js for this to work.
 */
Tx.fireDirectActionAjax = function(url, afterEffect)
{
    var options = {
            method: 'get',
            onSuccess: function(xmlhttp) {
                var responseText = Tx.getResponseText(xmlhttp);
                Tx.handleUpdatesFromDirectAction(responseText, afterEffect);
            }
    };

    Tx.ajax(url, options);
    return false;
};

/* handle the updates coming back from DirectActionService */
Tx.handleUpdatesFromDirectAction = function(responseText, afterEffect)
{
    var elementId;

    if (!Tx.handleAjaxExceptionPage(responseText)) {
        //responseText = Tx.stripHeadTagFromAjaxResponse(responseText);
        var domUpdates = Tx.evaluate(responseText);

        for (elementId in domUpdates) {
            if (domUpdates.hasOwnProperty(elementId)) {
                $(elementId).innerHTML = domUpdates[elementId];
            }
        }

        if (afterEffect) {
            afterEffect();
        }
    }
};

/* synchronously get data from the server via JSON */
Tx.ajaxJSON = function(url, postData)
{
    var result = null;
    var handleAjaxResponse = function(xmlhttp) {
        var jsonContents = Tx.getResponseText(xmlhttp);
        // take the jsonContents and turn it into a javascript data structure, assigning to the variable result
        // note that we put parentheses around the json string, so that the javascript parser knows that
        // it is an expression, not a statement.  It is important that the variable assignment is not in the eval
        // string so that any variable renaming done by javascript compression doesn't break us.
        result = Tx.evaluate(jsonContents);
    };

    var optionsDict;
    if (!postData) {
        optionsDict = {
            asynchronous: false,
            method: 'get',
            onSuccess: handleAjaxResponse
        };
    }
    else {
        optionsDict = {
            asynchronous: false,
            method: 'post',
            contentType:  'application/x-www-form-urlencoded',
            postBody: postData,
            onSuccess: handleAjaxResponse
        };
    }
    Tx.ajax(url, optionsDict);

    return result;
};

// See DOMData.java
Tx.getDOMData = function(parentNode)
{
    var child = Tx.findChildWithClass(parentNode, 'DOMDataHolder');
    // impl trick, the onblur func of the DOMDataHolder returns the data
    return child ? child.onblur() : null;
};

//See DOMData.java
Tx.getDOMFunction = function(parentNode)
{
    var child = Tx.findChildWithClass(parentNode, 'DOMDataHolder');
    // impl trick, the onfocus func of the DOMDataHolder holds the function we want
    return child ? child.onfocus : null;
};

/* Trick to execute embedded JS code in an AJAX response.  Norton sometimes strips <script> tags. */
Tx.findAndEvalJavascript = function(reponseContents)
{
    /*  // XXX Make this work -- see Bug #355
    if (document.isDevMode && reponseContents.indexOf("<script") != -1) {
        alert("Inline script detected in dialog response.  Norton security prevents the use of such script tags -- use @@script@@");
        alert(reponseContents);
    }
    */
    var scriptMarker = "@@script@@";
    var indexOfStart = 0;
    var indexOfMarker = 0;
    while ((indexOfMarker = reponseContents.indexOf(scriptMarker, indexOfStart)) !== -1) {
        indexOfStart = indexOfMarker + scriptMarker.length;
        var indexOfEnd = reponseContents.indexOf(scriptMarker, indexOfStart);
        var substring = reponseContents.substring(indexOfStart, indexOfEnd);
        // (Kevin 7/28/11) have to call eval explicitly here, rather than Tx.evaluate because of side effects?
        /*jslint evil: true */
        eval(substring);
        indexOfStart = indexOfEnd + 1;
    }
};


/*
 * Some geometry utils.  This section should get replaced by Prototype.js
 */


// Returns the viewport height, not including any horiz scrollbar.
Tx.windowInnerHeight = function()
{
    if (Tx.isIPad() || Tx.isIPhone()) {
        return Tx.isIPad() ? Tx.iPadHeight() : Tx.iPhoneHeight();
    }
    if (Tx.isSafari() || Tx.isChrome()) {
        return window.innerHeight;
    }
    return document.documentElement.clientHeight;
};

// Returns the viewport width, not including any vertical scrollbar.
Tx.windowInnerWidth = function()
{
    if (Tx.isIPad() || Tx.isIPhone()) {
        return Tx.isIPad() ? Tx.iPadWidth() : Tx.iPhoneWidth();
    }
    return document.documentElement.clientWidth;
};

Tx.clientWidth = function(w) 
{
    return this.clientFilterResults (
            w.innerWidth ? w.innerWidth : 0,
            w.document.documentElement ? w.document.documentElement.clientWidth : 0,
            w.document.body ? w.document.body.clientWidth : 0
    );
}

Tx.clientHeight = function(w) 
{
    return this.clientFilterResults (
            w.innerHeight ? w.innerHeight : 0,
            w.document.documentElement ? w.document.documentElement.clientHeight : 0,
            w.document.body ? w.document.body.clientHeight : 0
    );
}

Tx.clientFilterResults = function(n_win, n_docel, n_body) 
{
    var n_result = n_win ? n_win : 0;
    if (n_docel && (!n_result || (n_result > n_docel)))
            n_result = n_docel;
    return n_body && (!n_result || (n_result > n_body)) ? n_body : n_result;
}

// http://dev.conio.net/repos/prototype/src/position.js has alternative versions of these
Tx.absoluteTop = function(element)
{
    var theTop = 0;
    var parentElement = element;
    do {
        theTop += parentElement.offsetTop;
        // See bug 1165.  Never adjust for scrolXXX on the topmost (HTML or BODY) because that is document scroll
        if (parentElement.offsetParent) {
            theTop -= parentElement.scrollTop;
        }
        parentElement = parentElement.offsetParent;
    } while (parentElement);
    return theTop;
};

Tx.absoluteLeft = function(element)
{
    var theLeft = 0;
    var parentElement = element;
    do {
        theLeft += parentElement.offsetLeft;
        // See bug 1165.  Never adjust for scrolXXX on the topmost (HTML or BODY) because that is document scroll
        if (parentElement.offsetParent) {
            theLeft -= parentElement.scrollLeft;
        }
        parentElement = parentElement.offsetParent;
    } while (parentElement);
    return theLeft;
};

Tx.absoluteRight = function(element)
{
    return Tx.absoluteLeft(element) + element.offsetWidth;
};

Tx.absoluteBottom = function(element)
{
    return Tx.absoluteTop(element) + element.offsetHeight;
};

Tx.getDocumentElement = function()
{
    return (Tx.isSafari() || Tx.isChrome()) ? document.body : document.documentElement;
};

Tx.windowScrollTop = function()
{
    var documentElement = Tx.getDocumentElement();
    return documentElement.scrollTop;
};

Tx.windowScrollLeft = function()
{
    var documentElement = Tx.getDocumentElement();
    return documentElement.scrollLeft;
};

Tx.getWindowTopEdge = function()
{
    var topEdge = Tx.windowScrollTop();
    return topEdge;
};

Tx.getWindowBottomEdge = function()
{
    var topEdge = Tx.windowScrollTop();
    var windowHeight = Tx.windowInnerHeight();
    var windowBottomEdge = windowHeight + topEdge;
    return windowBottomEdge;
};

Tx.getWindowLeftEdge = function()
{
    var leftEdge = Tx.windowScrollLeft();
    return leftEdge;
};

Tx.getWindowRightEdge = function()
{
    var leftEdge = Tx.windowScrollLeft();
    var windowWidth = Tx.windowInnerWidth();
    var windowRightEdge = windowWidth + leftEdge;
    return windowRightEdge;
};

Tx.availableSpaceRight = function(element, leftPos)
{
    var windowRightEdge = Tx.getWindowRightEdge();
    var elementRightEdge = element.clientWidth + leftPos;
    var availableSpace = windowRightEdge - elementRightEdge;
    return availableSpace;
};

Tx.availableSpaceBottom = function(element, topPos)
{
    var windowBottomEdge = Tx.getWindowBottomEdge();
    var elementBottomEdge = element.clientHeight + topPos;
    var availableSpace = windowBottomEdge - elementBottomEdge;
    return availableSpace;
};

Tx.availableSpaceLeft = function(leftPos)
{
    var windowLeftEdge = Tx.getWindowLeftEdge();
    var availableSpace = leftPos - windowLeftEdge;
    return availableSpace;
};

Tx.availableSpaceTop = function(topPos)
{
    var windowTopEdge = Tx.getWindowTopEdge();
    var availableSpace = topPos - windowTopEdge;
    return availableSpace;
};

Tx.isOffscreenRight = function(element, leftPos)
{
    var availableSpace = Tx.availableSpaceRight(element, leftPos);
    return availableSpace < 0;
};

Tx.isOffscreenBottom = function(element, topPos)
{
    var availableSpace = Tx.availableSpaceBottom(element, topPos);
    return availableSpace < 0;
};

/**
*  The Tx.coverDivHeight/Width are used for computing the max width/height of a given document
*  and will use the Tx.windowInnerHeight/Width is that's greater than the document's width/height.
*/
Tx.coverDivHeight = function()
{
    return Math.max(Tx.windowInnerHeight(), Tx.getDocumentElement().scrollHeight) + 'px';
};

Tx.coverDivWidth = function()
{
    return Math.max(Tx.windowInnerWidth(), Tx.getDocumentElement().scrollWidth) + 'px';
};

Tx.createCoverDiv = function(className, zIndex, hidden)
{
    var coverDiv = document.createElement('div');
    var coverDivStyle = coverDiv.style;
    if (!className) {
        coverDivStyle.backgroundColor = 'white';
    }
    else {
        coverDiv.className = className;
    }
    coverDivStyle.position = 'absolute';
    coverDivStyle.top = '0px';
    coverDivStyle.left = '0px';
    coverDivStyle.zIndex = zIndex;
    if (hidden) {
        Element.setOpacity(coverDiv, 0.0);
        coverDivStyle.width = '100%';
        coverDivStyle.height = '100%';
    }
    else {
        coverDivStyle.width = Tx.coverDivWidth();
        coverDivStyle.height = Tx.coverDivHeight();
    }
    return coverDiv;
};

/**
*  This function is used by Ajax hanlder routines to detect and display an ExceptionPage response.
*  If an ExceptionPage response is detected, this covers the existing page with a div and puts the
*  body of the ExceptionPage into this div.  You cannot viewSource to see the exception, so you can
*  shift-click in the upper-right corner of the page to see the exception.
*/
Tx.handleAjaxExceptionPage = function(dialogContents)
{
    var isExceptionPage = false;
    var exceptionMarker = "<!-- ExceptionPage -->";
    var prefixSubstring = dialogContents.substring(0, exceptionMarker.length);
    if (prefixSubstring === exceptionMarker) {
        isExceptionPage = true;
        var bodyMarker = "<body";
        var indexOfBodyStart = dialogContents.indexOf(bodyMarker);
        var indexOfBodyEnd = dialogContents.indexOf("</body>", indexOfBodyStart);
        var bodyContents = dialogContents.substring(indexOfBodyStart, indexOfBodyEnd);
        var coverDiv = Tx.createCoverDiv(null, 1000, false);
        document.body.appendChild(coverDiv);
        coverDiv.innerHTML = bodyContents;
        window.scrollTo(0,0);
    }
    return isExceptionPage;
};

/**
 * ALWAYS use this to get the responseText from an XMLHttpRequest.
 * as it turns out safari sometimes returns null rather than empty string
 * for empty responses.  See bug 319.
 */
Tx.getResponseText = function(xmlhttp)
{
    if (xmlhttp && xmlhttp.responseText) {
        return xmlhttp.responseText;
    }
    return "";
};

/**
 * Cookie utilities
 */
Tx.setCookie = function(name, value, daysToExpire, domain)
{
    var cookieStr = name + "=" + value;
    if (daysToExpire) {
        var date = new Date();
        date.setTime(date.getTime() + daysToExpire*24*60*60*1000);
        cookieStr += "; expires="+date.toGMTString();
    }
    cookieStr += "; path=/";
    if (domain) {
        cookieStr += "; domain="+domain;
    }
    document.cookie = cookieStr;
};

Tx.getCookie = function(name)
{
    var start = document.cookie.indexOf(name + "=");
    if (start > -1) {
        start += name.length + 1;
        var end = document.cookie.indexOf(";", start);
        if (end === -1) {
            end = document.cookie.length;
        }
        return decodeURIComponent(document.cookie.substring(start, end));
    }
    return null;
};

Tx.clearCookie = function(name, domain)
{
    Tx.setCookie(name, "", -365, domain);
};

// Returns the first text field in a form element or null if none exists.
Tx.findFirstTextField = function(formElement)
{
    var firstTextField = null;
    var inputElementsArray = formElement.getElementsByTagName('INPUT');
    var arrayLength = inputElementsArray.length;
    var index;
    for (index = 0; index < arrayLength; index++) {
        var inputElement = inputElementsArray[index];
        if (inputElement.type === 'text' && !inputElement.readOnly) {
            firstTextField = inputElement;
            break;
        }
    }
    return firstTextField;
};

Tx.findChildTagWithId = function(element, tagName, targetId)
{
    var childArray = element.getElementsByTagName(tagName);
    if (childArray !== null && childArray !== undefined) {
        var childArrayLength = childArray.length;
        var index;
        for (index = 0; index < childArrayLength; index++) {
            var child = childArray[index];
            if (child.id === targetId) {
                return child;
            }
        }
    }
    return null;
};

Tx.findChildWithId = function(element, targetId)
{
    var child = $(element).down('#'+targetId);
    return child || null;
};

Tx.findChildWithClass = function(element, className)
{
    var child = $(element).down('.'+className);
    return child || null;
};

Tx.findFirstNontextChild = function(element)
{
    var children = element.childNodes;
    var childrenLength = children.length;
    var index;
    for (index = 0; index < childrenLength; index++) {
        var child = children[index];
        if (child.nodeName !== "#text") {
            return child;
        }
    }
    return null;
};

Tx.findParentWithClass = function(element, className)
{
    var parent = $(element).up('.'+className);
    return parent || null;
};

/**
* This function works for getting the window height info when setting the cookie.  The function Tx.windowInnerHeight()
* does not work properly for the inline js code which is emitted in BrowsePage.html.  We must use this same function
* for all comparisons of window height where the product cell size is involved.
*/
Tx.windowSizeForCookie = function()
{
    // Note that Tx.windowInnerHeight() doesn't work properly when used in this context (probably too early for things to be setup properly).

    return Tx.isIE() ? document.documentElement.offsetHeight : window.innerHeight;
};

Tx.iPadHeight = function()
{
    return (window.orientation === 0 || window.orientation === 180) ? 1300 : 740;
};

Tx.iPhoneHeight = function()
{
    return 740;
};

Tx.iPadWidth = function()
{
    return 1024;
};

Tx.iPhoneWidth = function()
{
    return 1024;
};

Tx.resetWindowSizeCookie = function(domain)
{
    var innerHeight = Tx.windowSizeForCookie();
    // Note that "win.h" must be kept in sync with same token in UserState.java
    // Note that closing token ';' must be kept in sync with same token in UserState.java
    // Note that cookie name 'sc5' must be kept in sync with same token in StoreUtil.WindowInfoCookieName
    // Note that we cannot use special chars in cookie value as tomcat > 5.5.25 does not like it
    // https://issues.apache.org/bugzilla/show_bug.cgi?id=44679
    Tx.setCookie('sc5', "win.h|" + innerHeight + "|", 0, domain);
};

Tx.stripHeadTagFromAjaxResponse = function(responseText)
{
    if (!responseText) {
        return null;
    }

    // To avoid searches on big strings
    var responsePrefix = responseText.length > 30 ? responseText.substring(0, 30) : responseText;
    var openHeadTagIndex = responsePrefix.indexOf('<head>');
    if (openHeadTagIndex === -1) {
        openHeadTagIndex = responsePrefix.indexOf('<HEAD>');
    }

    if (openHeadTagIndex === -1) {
        return responseText;
    }

    var closeHeadTagIndex = responseText.indexOf('</head>');
    if (closeHeadTagIndex === -1) {
        closeHeadTagIndex = responseText.indexOf('</HEAD>');
    }

    if (closeHeadTagIndex === -1) {
        return responseText;
    }

    closeHeadTagIndex += 7;

    var strippedResponse = responseText.substring(0, openHeadTagIndex) + responseText.substring(closeHeadTagIndex);
    return strippedResponse;
};

Tx.windowRefresh = function(useReload)
{
    if (useReload)
    {
        // note that this works exactly like clicking the browser reload button
        // which means that the browser may complain that POSTs need to be redone
        window.location.reload();
    }
    else {
        var url = window.location.href;
        // need to strip off any anchor portion (doesn't work in Firefox)
        var index = url.indexOf('#');
        if (index > 0) {
            url = url.substring(0,index);
        }
        // need to remove any dialog parameter, so that we don't keep redisplaying a finished dialog
        url = Tx.removeParam(url, "dialog");
        window.location = url;
    }
};

Tx.setLocationAnchor = function(newAnchor)
{
    if (Tx.isSafari() && !window.history.replaceState) { // See bug 1132, 1464 re safari anchor support
        return;
    }
    var url = window.location.href;
    // get the hash - note that Firefox has a bug with location.hash, it doesn't deal with encoded characters properly
    var hashIndex = url.indexOf("#");
    var newUrl = hashIndex === -1 ? url : url.substring(0, hashIndex);

    // We never remove the hash because this will cause a reload.
    newUrl += "#";
    if (newAnchor && newAnchor.length > 0) {
        newUrl += newAnchor;
    }
    var currentTitle = document.title;
    window.location.replace(newUrl);

    if (Tx.isFirefox()) {   // work around bug where Firefox drops title when using history.js
        document.title = currentTitle;
    }
    if ((Tx.isSafari() || Tx.isChrome())) {
        window.history.replaceState(null, this.title);
    }
};

Tx.setLocationToChildLink = function(aparent,event)
{
    var links = aparent.getElementsByTagName("a");
    if (links !== null && links !== undefined && links.length > 0) {
        if (links[0].onclick !== null) {
            return;
        }
        window.location = links[0].href;
    }
    // in case the click was on the contained anchor, don't let it do normal action
    Event.stop(event);
};

Tx.getParameter = function(param)
{
    return Tx.getParameterFromUrl(param, window.location.search);
};

Tx.getParameterFromUrl = function(param, url)
{
    var i = url.indexOf(param + "=");
    if (i > -1) {
        i += param.length + 1;
        var j = url.indexOf("&", i);
        if (j === -1) {
            j = url.length;
        }
        return url.substring(i, j);
    }
    return null;
};

// Appends a URL param, creating a duplicate if already exists (used for our fl= params)
Tx.appendParam = function(url, param, value)
{
    return url + (url.indexOf('?') > -1 ? '&' : '?') + param + '=' + value;
};

// Replace a URL param, appending to existing value if present, or clearing if new value is empty
// E.g. given 'shopstyle.com?a=123&b=456', 'a', 'xyz', returns 'shopstyle.com?a=xyz+123&b=456'
Tx.updateParam = function(url, param, value)
{
    var start = url.indexOf(param + '=');
    if (start > -1) {
        var end = url.indexOf('&', start);
        var paramLen = param.length + 1;
        var newValue = value === '' ? '' : value + '+' + (end > -1 ? url.substring(start + paramLen, end) : url.substring(start + paramLen));
        return url.substring(0, start) + param + '=' + newValue + (end > -1 ? url.substring(end) : '');
    }
    else {
        return url + (url.indexOf('?') > -1 ? '&' : '?') + param + '=' + value;
    }
};

// Replace a URL param, replacing an existing value if present, or clearing if new value is empty
// E.g. given 'shopstyle.com?a=123&b=456', 'a', 'xyz', returns 'shopstyle.com?a=xyz&b=456'
Tx.replaceParam = function(url, param, value)
{
    var start = url.indexOf(param + '=');
    if (start > -1) {
        var end = url.indexOf('&', start);
        return url.substring(0, start) + param + '=' + value + (end > -1 ? url.substring(end) : '');
    }
    else {
        return url + (url.indexOf('?') > -1 ? '&' : '?') + param + '=' + value;
    }
};

// remove a parameter from a url
Tx.removeParam = function(url, param)
{
    var start = url.indexOf(param + '=');
    if (start > -1) {
        var end = url.indexOf('&', start);
        // strip out the parameter
        url = url.substring(0, start) + (end > -1 ? url.substring(end) : '');
        // don't leave a trailing parameter token (the one that preceeded removed param)
        if (url.charAt(url.length - 1) === '&' || url.charAt(url.length - 1) === '?') {
            url = url.substring(0, url.length-1);
        }
    }

    return url;
};

// Handle inconsistency between IE and other browsers about what URLs come in with encoded special chars.
// If URL already contains a % char, this just returns it, otherwise calls encodeURI which encodes
// special chars but leaves URL separators / ? & alone.
Tx.encodeUriIfNeeded = function(uri)
{
    return (uri.indexOf('%') == -1) ? encodeURI(uri) : uri;
};

Tx.setAllCheckboxes = function(root, flag)
{
    var inputs = root.getElementsByTagName('input');
    var i;
    for (i = 0; i !== inputs.length; i++) {
        if (inputs[i].type === "checkbox") {
            inputs[i].checked = flag;
        }
    }
};

Tx.PopupContext = function()
{
    this._popupParent = null;
    this._timeout = null;
    this._touchOpened = null;
    this._popupChildId = null;
    this._popEnterTime = 0;
    this._topOffset = 0;
    this._leftOffset = 0;
    this._popupParentClass = null;
    // for observers of menu events
    this.observers = new TxEventObserver();
};

Tx.PopupContext.prototype.mdescription = "Tx.PopupContext class";

Tx.PopupContext.prototype.touchPopup = function(popupParent, event, childId, topOffset, leftOffset, leftAlignElement, positionRelative)
{
    if (this._touchOpened !== null && childId !== this._touchOpened) {
        // touched a different menu
        this.closePopup();
    }

    if (this._touchOpened === null) {
        // opening a menu
        this._touchOpened = childId;
        Event.stop(event);
        this.initiatePopup(popupParent, event, childId, topOffset, leftOffset, leftAlignElement, positionRelative, 0);
    }
    else {
        // let touch through to link
        this.closePopup();
        Event.stop(event);
    }
};

Tx.PopupContext.prototype.initiatePopup = function(popupParent, event, childId, topOffset, leftOffset, leftAlignElement, positionRelative, popupDelay)
{
    //trace('Tx. initiate popup on ' + event.target);
    if (this._popupParent && popupDelay !== 0) {
        if (this._popupParent === popupParent) {
            Event.stop(event);
            return;
        } else {
            this.closePopup();
        }
    }
    if ($(childId) === null) {
        return;
    }
    this._popupParent = popupParent;
    this._popupChildId = childId;
    this._topOffset = topOffset;
    this._positionRelative = positionRelative;
    if (leftAlignElement !== '') {
        this._leftOffset = $(leftAlignElement).offsetLeft - 12;
    } else {
        this._leftOffset = leftOffset;
    }
    this._popupParentClass = popupParent.className;

    if (popupDelay > 0) {
        Event.observe(document.body, "mouseover", Tx.popup_handleMouseOverBody, false);
        Event.observe(this._popupChildId, "mouseover", Tx.popup_handleMouseOverPopup, false);
    }

    var handlerFunction = function() {
        //trace('Tx. handler');
        Tx.popupContext.openPopup();
        Tx.popupContext._timeout = null;
    };
    if (popupDelay > 0) {
        this._timeout = window.setTimeout(handlerFunction, popupDelay);
    } else {
        Tx.popupContext.openPopup();
    }

    Event.stop(event);
};

/**
    Make the child popup div visible, and position directly beneath the parent.
*/
Tx.PopupContext.prototype.openPopup = function()
{
    var parentElement = this._popupParent;
    var menuElement = $(this._popupChildId);
    if (menuElement === null) {
        return;
    }
    //trace('Tx. show popup child');
    menuElement.style.position = 'absolute';
    menuElement.style.top = ((this._positionRelative ? (parentElement.offsetTop + parentElement.offsetHeight) : Tx.absoluteBottom(parentElement)) + this._topOffset) + 'px';
    var parentLeft = (this._positionRelative ? parentElement.offsetLeft : Tx.absoluteLeft(parentElement)) + this._leftOffset;

    var windowEdgeRight = Tx.windowInnerWidth() + Tx.windowScrollLeft();
    var elementWidth = Tx.absoluteRight(menuElement) - Tx.absoluteLeft(menuElement);
    if (parentLeft + elementWidth < windowEdgeRight) {
        menuElement.style.left = parentLeft+'px';
    }
    else {
        menuElement.style.left = (windowEdgeRight - elementWidth - 2)+'px';
    }
    menuElement.style.visibility='visible';

    // add a style to parent to show its current selection
    parentElement.className = this._popupParentClass + ' selected';
    this._popEnterTime = new Date().getTime();

    this.observers.notify("openPopup", menuElement);
};

Tx.PopupContext.prototype.closePopup = function()
{
    //trace('txp.closePopup');

    // cancel the timeout
    var popupContext = this;
    if (popupContext._timeout) {
        window.clearTimeout(popupContext._timeout);
        popupContext._timeout = null;
    }

    if (popupContext._popupParent) {
        // hide the menu
        popupContext._popupParent.className = popupContext._popupParentClass;
        var menuElement = $(this._popupChildId);
        Event.stopObserving(this._popupChildId, "mouseover", Tx.popup_handleMouseOverPopup, false);
        popupContext._popupParent = null;
        popupContext._popupChildId = null;
        popupContext._popupParentClass = null;
        if (menuElement !== null) {
            menuElement.style.visibility = 'hidden';
        }
    }
    this._touchOpened = null;
};

Tx.popup_handleMouseOverBody = function()
{
    var tDelta = new Date().getTime() - Tx.popupContext._popEnterTime;
    //trace('Tx. mouseoverbody ' + tDelta);
    Tx.popupContext._popEnterTime = 0;
    // if the body mouse-over happens immediately after popup mouse-over we want to ignore it because it is just a move from one item to another
    if (tDelta > 40) {
        Event.stopObserving(document.body, "mouseover", Tx.popup_handleMouseOverBody, false);
        Tx.popupContext.closePopup();
    }
};

Tx.popup_handleMouseOverPopup = function()
{
    Tx.popupContext._popEnterTime = new Date().getTime();
    //trace('Tx. mouseoverpopup ' + Tx.popupContext._popEnterTime);
};

Tx.popupContext = new Tx.PopupContext();

Tx.addEventListenerToElement = function(element, eventName, handlerFunction, useCapture)
{
    if (element.addEventListener === undefined) {
        element.attachEvent("on" + eventName, handlerFunction);
    }
    else {
        element.addEventListener(eventName, handlerFunction, useCapture);
    }
};

// for handling our own named events
function TxEventObserver()
{
}

// add an observer specified for a given event name
TxEventObserver.prototype.addObserver = function(eventName, observer)
{
    if (!this[eventName]) {
        this[eventName] = [];
    }
    this[eventName].push(observer);
    // return the index as the id of the observer, for possible removal
    return this[eventName].length-1;
};

TxEventObserver.prototype.removeObserver = function(eventName, obsIndex)
{
    var observers = this[eventName];
    if (observers) {
        observers[obsIndex] = null;
    }
};

TxEventObserver.prototype.notify = function(eventName, arg)
{
    var observers = this[eventName];
    var ii;
    if (observers) {
        for (ii = 0; ii < observers.length; ii++) {
            if (observers[ii]) {
                observers[ii](arg);
            }
        }
    }
};

// convert touch events to equivalent mouse events in an element
function TouchToMouse(element)
{
    if (!$(element)) {
        return;
    }
    this.moving = false;
    this.element = element;

    $(this.element).observe('touchstart', this.start.bind(this));
    this.moveBind = this.move.bind(this);
    this.endBind = this.end.bind(this);
}

TouchToMouse.prototype.start = function(event)
{
    Event.stop(event);
    if (this.moving) { return;}
    this.moving = true;
    $(this.element).observe('touchmove', this.moveBind);
    $(this.element).observe('touchend', this.endBind);

    this._cloneEvent('mousedown', event.changedTouches[0]);
};

TouchToMouse.prototype.move = function(event)
{
    Event.stop(event);
    if (!this.moving) {return;}

    this._cloneEvent('mousemove', event.changedTouches[0]);
};

TouchToMouse.prototype.end = function(event)
{
    Event.stop(event);
    if (!this.moving) {return;}
    this.moving = false;
    $(this.element).stopObserving('touchmove', this.moveBind);
    $(this.element).stopObserving('touchend', this.endBind);
    this._cloneEvent('mouseup', event.changedTouches[0]);
};

TouchToMouse.prototype._cloneEvent = function(type, touch)
{
    var event = document.createEvent("MouseEvents");
    event.initMouseEvent(type, true, true, window, 0, touch.screenX, touch.screenY,
            touch.clientX, touch.clientY,
            false, false, false, false, 0, null);
    touch.target.dispatchEvent(event);
};




