Source: helper/functions.js

/**
 * @module functions
 */

/* jshint -W098 */
/* global MenubarActions */
/* global KEYBOARD_LISTENER */
/* global popupHandler */
/* global buildBASEURLForPageFile */
/* global ActiveXObject */
/* global i18n */
/* global printStackTrace */

/**
 * Translation method - checks for the i18n to be present and returns the key
 * @param   {string} key          Key to look up in the translation table
 * @param   {string} defaultValue value if key was not set
 * @returns {string} translated value
 */
function getTranslation(key, defaultValue) {
    if (typeof i18n == 'undefined') {
        return defaultValue || key;
    }

    return typeof i18n[key] != 'undefined' ? i18n[key] : defaultValue || key;
}

/**
 * Handy shortcut to document.getElementById
 * 
 * This function was taken from the prototype library
 * 
 * {@link http://prototype.conio.net/}
 * @param {object} String or Array of IDs to look for with getElementById
 * @returns {object} the object denoted by the given ID
 * @public
 */
function getId() {
    var elements = [], ensureElement = false, args = Object.values(arguments);

	if ( args.length > 0 && typeof args[args.length-1] == "boolean" ) {
		ensureElement = args.pop();
	}

    for (var i = 0; i < args.length; i++) {
        var element = args[i];
        if (typeof element == 'string') {
            element = document.getElementById(element);
        }

        if (args.length == 1) {
            return ensureElement ? element || new Element() : element;
		}

        elements.push(element);
    }

    return elements;
}

/***
 * Helper Object for the IE which is too dump to do it on his own
 */
if (!Object.keys) {
    Object.keys = function (o) {
        if (o !== Object(o)) {
            throw new TypeError('Object.keys called on non-object');
        }

        var ret = [],
            p;
        for (p in o) {
            if (Object.prototype.hasOwnProperty.call(o, p)) {
                ret.push(p);
            }
        }

        return ret;
    };
}

if ('undefined' == typeof Object.hasOwnProperty) {
    /**
     * Checks if property is derived from prototype, applies method if it is not
     * exists
     * 
     * @param {string}  property name
     * @return {boolean} true if prototyped
     * @public
     */
    Object.prototype.hasOwnProperty = function (prop) {
        return !('undefined' == typeof this[prop] || this.constructor && this.constructor.prototype[prop] && this[prop] === this.constructor.prototype[prop]);
    };
}

/*----------------------------------------------------------------------
 XRegExp 0.1, by Steven Levithan <http://stevenlevithan.com>
 MIT-style license
 ------------------------------------------------------------------------
 Adds support for the following regular expression features:
 - The "s" flag: Dot matches all (a.k.a, single-line) mode.
 - The "x" flag: Free-spacing and comments mode.

 XRegExp also offers consistent, cross-browser handling when "]" is used
 as the first character within a character class (e.g., "[]]" or "[^]]").
 ----------------------------------------------------------------------*/
/**
 * Extended Regular Expression matching with support for the "s" and "x" flags
 * 
 * @param   {string} pattern regular expression pattern
 * @param   {string} flags   flags to be set
 * @returns {RegExp} the RegExp object
 */
var XRegExp = function (pattern, flags) {
        if (!flags) {
            flags = "";
        }

        /*
         * If the "free-spacing and comments" modifier (x) is enabled, replace
         * unescaped whitespace as well as unescaped pound signs (#) and any
         * following characters up to and including the next newline character (\n)
         * with "(?:)". Using "(?:)" instead of just removing matches altogether
         * prevents, e.g., "\1 0" from becoming "\10" (which has different meanings
         * depending on context). None of this applies within character classes,
         * which are unaffected even when they contain whitespace or pound signs
         * (which is consistent with pretty much every library except
         * java.util.regex).
         */
        if (flags.indexOf("x") !== -1) {
            pattern = pattern.replace(XRE.re.xMod, function ($0, $1, $2) {
                // If $2 is an empty string or its first character is "[", return
                // the match unviolated (an effective no-op).
                return (/[^[]/.test($2.charAt(0)) ? $1 + "(?:)" : $0);
            });
        }

        /*
         * Two steps (the order is not important):
         * 
         * 1. Since a regex literal will be used to return the final regex
         * function/object, replace characters which are not allowed within regex
         * literals (carriage return, line feed) with the metasequences which
         * represent them (\r, \n), accounting for both escaped and unescaped
         * literal characters within pattern. This step is only necessary to support
         * the XRE.overrideNative() method, since otherwise the RegExp constructor
         * could be used to return the final regex.
         * 
         * 2. When "]" is the first character within a character class, convert it
         * to "\]", for consistent, cross-browser handling. This is included to
         * workaround the following Firefox quirks (bugs?): - "[^]" is equivalent to
         * "[\S\s]", although it should throw an error. - "[^]]" is equivalent to
         * "[\S\s]]", although it should be equivalent to "[^\]]" or "(?!])[\S\s]". -
         * "[]" is equivalent to "(?!)" (which will never match), although it should
         * throw an error. - "[]]" is equivalent to "(?!)]" (which will never
         * match), although it should be equvialent to "[\]]" or "]".
         * 
         * Note that this step is not just an extra feature. It is in fact required
         * in order to maintain correctness without the aid of browser sniffing when
         * constructing the regexes which deal with character classes
         * (XRE.re.chrClass and XRE.re.xMod). They treat a leading "]" within a
         * character class as a non-terminating, literal character.
         */
        pattern = pattern.replace(XRE.re.badChr, function ($0, $1, $2) {
            return $1 + $2.replace(/\r/, "\\r").replace(/\n/, "\\n");
        }).replace(XRE.re.chrClass, function ($0, $1, $2) {
            return $1 + $2.replace(/^(\[\^?)]/, "$1\\]");
        });

        // If the "dot matches all" modifier (s) is enabled, replace unescaped dots
        // outside of character classes with [\S\s]
        if (flags.indexOf("s") !== -1) {
            pattern = pattern.replace(XRE.re.chrClass, function ($0, $1, $2) {
                return $1.replace(XRE.re.sMod, function ($0, $1, $2) {
                    return $1 + ($2 === "." ? "[\\S\\s]" : "");
                }) + $2;
            });
        }

        try {
            // Use an evaluated regex literal to return the regular expression, in
            // order to support the XRE.overrideNative() method.
            return new RegExp(pattern, flags.replace(/[sx]+/g, ""));
        } catch (e) {
            alert(e);
            throw e;
        }
    },
    XRE = {
        overrideNative: function () {
            /*
             * Override the global RegExp constructor/object with the enhanced
             * XRegExp constructor. This precludes accessing properties of the last
             * match via the global RegExp object. However, those properties are
             * deprecated as of JavaScript 1.5, and the values are available on
             * RegExp instances or via the exec() method.
             */
            /* jshint -W020 */
            RegExp = XRegExp;
            /* jshint +W020 */
        },
        re: {
            chrClass: /((?:[^[\\]+|\\(?:[\S\s]|$))*)((?:\[\^?]?(?:[^\\\]]+|\\(?:[\S\s]|$))*]?)?)/g,
            xMod: /((?:[^[#\s\\]+|\\(?:[\S\s]|$))*)((?:\[\^?]?(?:[^\\\]]+|\\(?:[\S\s]|$))*]?|\s*#[^\n\r]*[\n\r]?\s*|\s+)?)/g,
            sMod: /((?:[^\\.]+|\\(?:[\S\s]|$))*)(\.?)/g,
            badChr: /((?:[^\\\r\n]+|\\(?:[^\r\n]|$(?!\s)))*)\\?([\r\n]?)/g
        }
    };
/*
 * XRE.re is used to cache the more complex regexes so they don't have to be
 * recompiled every time XRegExp runs. Note that the included regexes match
 * anything (if they didn't, they would have to be rewritten to avoid
 * catastrophic backtracking on failure). It's the backreferences, as well as
 * where one match ends and the next begins, that's important. Also note that
 * the regexes are exactly as they are in order to account for all kinds of
 * caveats involved in interpreting and working with JavaScript regexes. Do not
 * modify them!
 */

RegExp.escape = function (text) {
    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
};

/**
 * Do a replace with a callback function
 * @param   {string}   pattern  RegExp pattern
 * @param   {function} callback function to call for matches
 * @param   {string}   subject  data to check for the pattern
 * @param   {number}   limit    how many hits we want at most. -1 or not set for unlimited
 * @returns {string}   the result
 */
function preg_replace_callback(pattern, callback, subject, limit) {
    // Perform a regular expression search and replace using a callback
    // 
    // discuss at: http://geekfg.net/
    // + original by: Francois-Guillaume Ribreau (http://fgribreau)
    // * example 1:
    // preg_replace_callback("/(\\@[^\\s,\\.]*)/ig",function(matches){return
    // matches[0].toLowerCase();},'#FollowFriday @FGRibreau @GeekFG',1);
    // * returns 1: "#FollowFriday @fgribreau @GeekFG"
    // * example 2:
    // preg_replace_callback("/(\\@[^\\s,\\.]*)/ig",function(matches){return
    // matches[0].toLowerCase();},'#FollowFriday @FGRibreau @GeekFG');
    // * returns 2: "#FollowFriday @fgribreau @geekfg"

    limit = !limit ? -1 : limit;

    var _check = pattern.substr(0, 1),
        _flag = pattern.substr(pattern
            .lastIndexOf(_check) + 1),
        _pattern = pattern.substr(1, pattern
            .lastIndexOf(_check) - 1),
        reg = new RegExp(_pattern, _flag),
        res = [],
        x = 0,
        list = [],
        ret = subject;

    String.prototype.repeat = function (num) {
        return new Array(num + 1).join(this);
    };

    if (limit === -1) {
        var tmp = [];

        do {
            tmp = reg.exec(subject);
            if (tmp !== null) {
                res.push(tmp);
            }
        } while (tmp !== null && _flag.indexOf('g') !== -1);
    } else {
        res.push(reg.exec(subject));
    }

    for (x = res.length - 1; x > -1; x--) { // explore match
        if (!list[res[x][0]]) {
            ret = ret.replace((new RegExp(RegExp.escape(res[x][0]), "g")),
                callback(res[x]));
            list[res[x][0]] = true;
        }
    }
    return ret;
}

/**
 * findAndReplaceDOMText v 0.3.0
 * @author James Padolsey http://james.padolsey.com
 * @license http://unlicense.org/UNLICENSE
 *
 * Matches the text of a DOM node against a regular expression
 * and replaces each match (or node-separated portions of the match)
 * in the specified element.
 */
window.findAndReplaceDOMText = (function () {

    /** 
     * findAndReplaceDOMText
     * 
     * Locates matches and replaces with replacementNode
     *
     * @param {RegExp} regex The regular expression to match
     * @param {Node} node Element or Text node to search within
     * @param {String|Element|Function} replacementNode A NodeName,
     *  Node to clone, or a function which returns a node to use
     *  as the replacement node.
     * @param {Number} [captureGroup] A number specifiying which capture
     *  group to use in the match. (optional)
     * @param {Function} [elFilter] A Function to be called to check whether to
     *  process an element. (returning true = process element,
     *  returning false = avoid element)
     */
    function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, elFilter) {

        var m, matches = [],
            text = _getText(node, elFilter);
        var replaceFn = _genReplacer(replacementNode);

        if (!text) {
            return;
        }

        if (regex.global) {
            while ((m = regex.exec(text)) !== null) {
                matches.push(_getMatchIndexes(m, captureGroup));
            }
        } else {
            m = text.match(regex);
            matches.push(_getMatchIndexes(m, captureGroup));
        }

        if (matches.length) {
            _stepThroughMatches(node, matches, replaceFn, elFilter);
        }
    }

    /**
     * Gets the start and end indexes of a match
     */
    function _getMatchIndexes(m, captureGroup) {

        captureGroup = captureGroup || 0;

        if (!m[0]) { throw 'findAndReplaceDOMText cannot handle zero-length matches'; }

        var index = m.index;

        if (captureGroup > 0) {
            var cg = m[captureGroup];
            if (!cg) { throw 'Invalid capture group'; }
            index += m[0].indexOf(cg);
            m[0] = cg;
        }

        return [index, index + m[0].length, [m[0]]];
    }

    /**
     * Gets aggregate text of a node without resorting
     * to broken innerText/textContent
     */
    function _getText(node, elFilter) {

        if (node.nodeType === 3) {
            return node.data;
        }

        if (elFilter && !elFilter(node)) {
            return '';
        }

        var txt = '';

        if ((node = node.firstChild) !== null) {
            do {
                txt += _getText(node, elFilter);
            } while ((node = node.nextSibling) !== null);
        }

        return txt;

    }

    /** 
     * Steps through the target node, looking for matches, and
     * calling replaceFn when a match is found.
     */
    function _stepThroughMatches(node, matches, replaceFn, elFilter) {

        var startNode,
            endNode,
            startNodeIndex,
            endNodeIndex,
            innerNodes = [],
            atIndex = 0,
            curNode = node,
            matchLocation = matches.shift(),
            matchIndex = 0,
            doAvoidNode;

        out: while (true) {

            if (curNode.nodeType === 3) {

                if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
                    // We've found the ending
                    endNode = curNode;
                    endNodeIndex = matchLocation[1] - atIndex;
                } else if (startNode) {
                    // Intersecting node
                    innerNodes.push(curNode);
                }

                if (!startNode && curNode.length + atIndex > matchLocation[0]) {
                    // We've found the match start
                    startNode = curNode;
                    startNodeIndex = matchLocation[0] - atIndex;
                }

                atIndex += curNode.length;
            }

            doAvoidNode = curNode.nodeType === 1 && elFilter && !elFilter(curNode);

            if (startNode && endNode) {
                curNode = replaceFn({
                    startNode: startNode,
                    startNodeIndex: startNodeIndex,
                    endNode: endNode,
                    endNodeIndex: endNodeIndex,
                    innerNodes: innerNodes,
                    match: matchLocation[2],
                    matchIndex: matchIndex
                });
                // replaceFn has to return the node that replaced the endNode
                // and then we step back so we can continue from the end of the 
                // match:
                atIndex -= (endNode.length - endNodeIndex);
                startNode = null;
                endNode = null;
                innerNodes = [];
                matchLocation = matches.shift();
                matchIndex++;
                if (!matchLocation) {
                    break; // no more matches
                }
            } else if (!doAvoidNode &&
                (curNode.firstChild || curNode.nextSibling)
            ) {
                // Move down or forward:
                curNode = curNode.firstChild || curNode.nextSibling;
                continue;
            }

            // Move forward or up:
            while (true) {
                if (curNode.nextSibling) {
                    curNode = curNode.nextSibling;
                    break;
                } else if (curNode.parentNode !== node) {
                    curNode = curNode.parentNode;
                } else {
                    break out;
                }
            }

        }

    }

    var reverts;
    /**
     * Reverts the last findAndReplaceDOMText process
     */
    findAndReplaceDOMText.revert = function revert() {
        for (var i = 0, l = reverts.length; i < l; ++i) {
            reverts[i]();
        }
        reverts = [];
    };

    /** 
     * Generates the actual replaceFn which splits up text nodes
     * and inserts the replacement element.
     */
    function _genReplacer(nodeName) {

        reverts = [];

        var makeReplacementNode;

        if (typeof nodeName != 'function') {
            var stencilNode = nodeName.nodeType ? nodeName : document.createElement(nodeName);
            makeReplacementNode = function (fill) {
                var clone = document.createElement('div'),
                    el;
                clone.innerHTML = stencilNode.outerHTML || new XMLSerializer().serializeToString(stencilNode);
                el = clone.firstChild;
                if (fill) {
                    el.appendChild(document.createTextNode(fill));
                }
                return el;
            };
        } else {
            makeReplacementNode = nodeName;
        }

        return function replace(range) {

            var startNode = range.startNode,
                endNode = range.endNode,
                matchIndex = range.matchIndex;

            if (startNode === endNode) {
                var node = startNode;
                if (range.startNodeIndex > 0) {
                    // Add `before` text node (before the match)
                    let before = document.createTextNode(node.data.substring(0, range.startNodeIndex));
                    node.parentNode.insertBefore(before, node);
                }

                // Create the replacement node:
                var el = makeReplacementNode(range.match[0], matchIndex, node);
                node.parentNode.insertBefore(el, node);

                if (range.endNodeIndex < node.length) {
                    // Add `after` text node (after the match)
                    let after = document.createTextNode(node.data.substring(range.endNodeIndex));
                    node.parentNode.insertBefore(after, node);
                }

                node.parentNode.removeChild(node);

                reverts.push(function () {
                    var pnode = el.parentNode;
                    pnode.insertBefore(el.firstChild, el);
                    pnode.removeChild(el);
                    pnode.normalize();
                });

                return el;

            } else {
                // Replace startNode -> [innerNodes...] -> endNode (in that order)
                let before = document.createTextNode(startNode.data.substring(0, range.startNodeIndex));
                let after = document.createTextNode(endNode.data.substring(range.endNodeIndex));
                var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex, range.match[0]);
                var innerEls = [];

                for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
                    var innerNode = range.innerNodes[i];
                    var innerEl = makeReplacementNode(innerNode.data, matchIndex, range.match[0]);
                    innerNode.parentNode.replaceChild(innerEl, innerNode);
                    innerEls.push(innerEl);
                }

                var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex, range.match[0]);

                startNode.parentNode.insertBefore(before, startNode);
                startNode.parentNode.insertBefore(elA, startNode);
                startNode.parentNode.removeChild(startNode);
                endNode.parentNode.insertBefore(elB, endNode);
                endNode.parentNode.insertBefore(after, endNode);
                endNode.parentNode.removeChild(endNode);

                reverts.push(function () {
                    innerEls.unshift(elA);
                    innerEls.push(elB);
                    for (var i = 0, l = innerEls.length; i < l; ++i) {
                        var el = innerEls[i];
                        var pnode = el.parentNode;
                        pnode.insertBefore(el.firstChild, el);
                        pnode.removeChild(el);
                        pnode.normalize();
                    }
                });

                return elB;
            }
        };

    }

    return findAndReplaceDOMText;

}());


/*******************************************************************************
 * HTTPRequest Method
 *******************************************************************************
 Simple AJAX Code-Kit (SACK)
 2005 Gregory Wild-Smith
 www.twilightuniverse.com

 Software licenced under a modified X11 licence, see documentation or authors
 website for more details
 ******************************************************************************/

var _requestMethod = "POST";

function SACK(file) {
    this.AjaxFailedAlert = getTranslation("ajax.failedAlert");
    this.requestFile = file;
    this.method = _requestMethod;
    this.headers = [];
    this.URLString = "";
    this.encodeURIString = false;
    this.execute = false;
    this.asynchronous = true;
    this.canceled = false;

    this.parameters = {};

    this.onLoading = function () {};
    this.onLoaded = function () {};
    this.onInteractive = function () {};
    this.onCompletion = function () {};
    this.afterCompletion = function () {};

    this.createAJAX = function () {
        try {
            this.xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e) {
            try {
                this.xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
            } catch (err) {
                this.xmlhttp = null;
            }
        }
        if (!this.xmlhttp && typeof XMLHttpRequest != "undefined") {
            this.xmlhttp = new XMLHttpRequest();
        }
        if (!this.xmlhttp) {
            this.failed = true;
        } else {
            try {
                if (this.xmlhttp.abort) {
                    $.Events.addEvent(this.xmlhttp, "abort", this.canceling);
                }
            } catch (e) {}
        }
    };

    this.canceling = function () {
        this.canceled = true;
    };

    this.setVar = function (name, value) {
        this.parameters[name] = value;
    };

    this.encVar = function (name, value) {
        var varString = encodeURIComponent(name) + "=" + encodeURIComponent(value);
        return varString;
    };

    this.parameterString = function (filter, doNotEncode) {

        // always sorted
        var keys = Object.keys(this.parameters).sort();
        if (filter) {
            keys = keys.filter(filter);
        }

        var result = [];
        var self = this;
        keys.forEach(function (value) {
        	if ( value == "timezone" ) {
        		self.headers[value] = self.parameters[value];
        	} else {
        		result.push(self.encodeURIString && !doNotEncode ? self.encVar( value, self.parameters[value]) : value + "=" + self.parameters[value]);
        	}
        });

        return result.join("&");
    };

    this.runResponse = function () {
        /* jshint -W061 */
        eval(this.response);
        /* jshint +W061 */
    };

    this.onLocalFileFunction = function () {
        throw getTranslation("ajax.localFileFailed");
    };

    this.runAJAX = function () {
        this.canceled = false;
        this.responseStatus = new Array(2);
        if (this.failed && this.AjaxFailedAlert) {
            alert(this.AjaxFailedAlert);
        } else {

            // Create this.URLString;
            this.URLString = this.parameterString();

            if (this.element) {
                this.elementObj = document.getElementById(this.element);
            }
            if (this.xmlhttp) {
                var self = this;
                if (this.method == "GET") {
                    var totalurlstring = this.requestFile + (this.requestFile.indexOf("?") != -1 ? "&" : "?") + this.URLString;
                    this.xmlhttp.open(this.method, totalurlstring, this.asynchronous);
                } else {
                    this.xmlhttp.open(this.method, this.requestFile, this.asynchronous);
                }
                if (this.method == "POST") {
                    try {
                        this.xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
                        for (var header in this.headers) {
                            if (!this.headers.hasOwnProperty(header)) { continue; }
                            this.xmlhttp.setRequestHeader(header, this.headers[header]);
                        }
                    } catch (e) {}
                }

                this.xmlhttp.onreadystatechange = function () {
                    switch (self.xmlhttp.readyState) {
                    case 1:
                        self.onLoading();
                        break;
                    case 2:
                        self.onLoaded();
                        break;
                    case 3:
                        self.onInteractive();
                        break;
                    case 4:
                        self.response = self.xmlhttp.responseText;
                        self.responseXML = self.xmlhttp.responseXML;
                        self.responseStatus[0] = self.xmlhttp.status;
                        self.responseStatus[1] = self.xmlhttp.statusText;
                        self.onCompletion();
                        if (self.execute) {
                            self.runResponse();
                        }
                        if (self.elementObj) {
                            var elemNodeName = self.elementObj.nodeName;
                            elemNodeName.toLowerCase();
                            if (elemNodeName == "input" || elemNodeName == "select" || elemNodeName == "option" || elemNodeName == "textarea") {
                                self.elementObj.value = self.response;
                            } else {
                                self.elementObj.innerHTML = self.response;
                            }
                        }
                        self.afterCompletion();
                        self.URLString = "";
                        break;
                    }
                };

                if (this.requestFile.substr(0, 4) == 'file' && (IEVersion().Version.toLowerCase() == 'na' || IEVersion().Version >= 10)) {
                    this.onLocalFileFunction();
                } else {
                    this.xmlhttp.send(this.URLString);
                }
            }
        }
    };
    this.createAJAX();
}

/*
 * Author: Rob Reid CreateDate: 20-Mar-09 Description: Little helper function to
 * return details about IE 8 and its various compatibility settings either use
 * as it is or incorporate into a browser object. Remember browser sniffing is
 * not the best way to detect user-settings as spoofing is very common so use
 * with caution.
 */
function IEVersion() {
    var _n = navigator,
        _w = window,
        _d = document;
    var version = "NA";
    var na = _n.userAgent;
    var ieDocMode = "NA";
    var ie8BrowserMode = "NA";
    // Look for msie and make sure its not opera in disguise
    if (/msie/i.test(na) && (!_w.opera)) {
        // also check for spoofers by checking known IE objects
        if (_w.attachEvent && _w.ActiveXObject) {
            // Get version displayed in UA although if its IE 8 running in 7 or
            // compat mode it will appear as 7
            version = (na.match(/.+ie\s([\d.]+)/i) || [])[1];
            // Its IE 8 pretending to be IE 7 or in compat mode
            if (parseInt(version) == 7) {
                // documentMode is only supported in IE 8 so we know if its here
                // its really IE 8
                if (_d.documentMode) {
                    version = 8; // reset? change if you need to
                    // IE in Compat mode will mention Trident in the useragent
                    if (/trident\/\d/i.test(na)) {
                        ie8BrowserMode = "Compat Mode";
                        // if it doesn't then its running in IE 7 mode
                    } else {
                        ie8BrowserMode = "IE 7 Mode";
                    }
                }
            } else if (parseInt(version) == 8) {
                // IE 8 will always have documentMode available
                if (_d.documentMode) {
                    ie8BrowserMode = "IE 8 Mode";
                }
            }
            // If we are in IE 8 (any mode) or previous versions of IE we check
            // for the documentMode or compatMode for pre 8 versions
            ieDocMode = (_d.documentMode) ? _d.documentMode : (_d.compatMode && _d.compatMode == "CSS1Compat") ? 7 : 5; // default
            // to
            // quirks
            // mode
            // IE5
        }
    } else if (/\sedg\//i.test(na) ) {
		version = (na.match(/.+\sedg\/([\d.]+)/i) || [])[1];
	}

    return {
        "UserAgent": na,
        "Version": version,
        "BrowserMode": ie8BrowserMode,
        "DocMode": ieDocMode
    };
}

IEVersion();

/**
 * Check for an online mode to provide some functions
 * 
 * @class
 * @param {function} function to be called if online
 * @return {object} the amIOnline object
 */
var amIOnline = (function () {

    var finishedFunction = [];
    var ajax = null;

    var evaluateFinishFunctions = function (isOnline) {
        amIOnline.isOnline = isOnline;
        if (!isOnline) {
            _requestMethod = "GET";
        }

        finishedFunction.forEach(function (onlineFunction) {
            if ( typeof onlineFunction == 'function' ) { onlineFunction(isOnline); }
        });

        // Not sure why, but I think the list of functions should be emptied after they have been called
        // if not it will result in multiple callings
        finishedFunction = [];
    };

    var runCheck = function () {
        ajax = new SACK(buildBASEURLForPageFile('reportserver', true));
        ajax.method = "HEAD";
        ajax.onCompletion = function () {
            // see #27363: diese Methode sollte vermutlich immer verwendet werden -sonst gibt es probleme mit nem legitimen 404 auf dem HelpDesk
            // 2014-08-21 - therefore nothing of the rest is needed. Simply commenting it out.
            // 2015-04-29 - application servers do not nesseccarily send their server string (weblogic), so we check the status code again. 204 can only be our own.
            var online = false;
            if (ajax.xmlhttp != null) {
                var server = typeof ajax.xmlhttp.getResponseHeader != 'undefined' ? ajax.xmlhttp.getResponseHeader('Server') : null;
                online = ajax.xmlhttp.status == 204 || (server !== null && server !== '');
            }

            debug("Check Online Completed. We are " + online ? 'online' : 'offline' + ".");
            evaluateFinishFunctions(online);
        };

        try {
            ajax.runAJAX();
        } catch (e) {
            debug("Offline because of catch.");
            ajax.responseStatus[0] = 500;
            ajax.responseStatus[1] = e;
            evaluateFinishFunctions(false);
        }
    };

    var _amIOnline = {
        isOnline: false,

        // Call this to check if we are online
        check: function (hasCheckedFunction) {
			if ( ajax && ( ajax.responseStatus[0] || !amIOnline.isOnline ) ) {
                hasCheckedFunction(amIOnline.isOnline);
			} else {
	            finishedFunction.push(hasCheckedFunction);

                // Run the check only if it is not running yet.
                if (!ajax) {
                    runCheck();
                }
            }
        },

        // This is for the tests.
        reset: function () {
            ajax = null;
            finishedFunction = [];
            _amIOnline.isOnline = false;
        }
    };

    _amIOnline.reset();

    return _amIOnline;
})();

/**
 * Checks a given string is a JSON representation 
 * @param   {string} data    String of potential JSON information
 * @param   {object} xmlhttp XMLHTTPObject to check for content type
 * @returns {object} null if not JSON, false if error, evaled JSON otherwise
 */
var dataAsJson = function (data, xmlhttp) {
    // HD #48538 - The error.json file may contain whitespaces of any kind
    // before or after the leading/trailing brackets ... in special cases ;)
    if (typeof data == "string" && data !== '' && ((xmlhttp != null && xmlhttp.getResponseHeader('Content-Type') != null && xmlhttp.getResponseHeader('Content-Type').indexOf('application/json') === 0) || data.match(new XRegExp("^{.*?}$", 's')))) {
        try {
            /* jshint -W061 */
            return eval('(' + data + ')');
            /* jshint +W061 */
        } catch (e) {
            // ERROR!
            new errorPacketHandler({
                errorHeader: getTranslation("jsonError.parserError"),
                errorMessage: e,
                JSONData: data
            });

            return false;
        }
    }

    // Something happened. Nothing there
    return null;
};

var _jsonErrorCounter = 0;
/**
 * Check if the given data is a JSON error
 * @param   {string}  data    String of potential JSON error
 * @param   {object}  xmlhttp XMLHTTPObject to check
 * @returns {boolean} if this was an error or not. Will raise the errorbox if it was one
 */
var dataHasJSONError = function (data, xmlhttp) {

    var json = dataAsJson(data, xmlhttp);
    if (json === false) {
        return true; // An Error occured and has been thrown up
    }

    if (json && json.errorCode) {

        _jsonErrorCounter++;
        if (_jsonErrorCounter <= 3) {
            if (json.errorCode == 2002) {
                // Page out of range. This means we could load the page 1 again.
                try {
                    (new MenubarActions()).setPage(1);
                } catch (e) {}
            }
        }

        // Was an error - should be handled differently
        new errorPacketHandler(json);
        return true;
    }

    // json = null -> no json
    _jsonErrorCounter = 0;
    return false;
};

/**
 * An error is being raised with a potential HTML Content
 * @param {string} data   the body of the error
 * @param {string} status the response status
 */
var handleErrorPage = function (data, status) {
    // This should be a super normal error - no checking via iframe
    // needed. If status = 0 it has been canceled. hasnt it?

    var title = (data||"").replace(new XRegExp('^.*?<title.*?>(.*?)<\/title.*?$', 'sgi'), "$1");
    var body = (data||"").replace(new XRegExp('^.*?<body.*?>(.*?)<\/body.*?$', 'sgi'), "$1");

    new errorPacketHandler({
        errorHeader: title.length > 0 ? title : getTranslation("loadPage.pageLoadingError"),
        errorMessage: body.length > 0 ? body : null,
        responseStatus: status,
        responseMessage: body.length === 0 ? data : null
    });
};

/**
 * A Dropdown Message. Will apear below the menu bar.
 * Can be disposed by click or disappears after 10 seconds
 * 
 * @param   {string} message the message to display
 * @returns {object} the container that displays the message
 */
var dropDownMessage = function (message) {

    var messageContainer = document.createElement('div');
    messageContainer.addClassName('dropDownMessage');
    messageContainer.addClassName('error');
    messageContainer.appendChild(document.createTextNode(message));
    getId('__contentwrapper').parentNode.appendChild(messageContainer);

    var remove = function () {
        messageContainer.addClassName('leaving');
        window.setTimeout(function () {
            messageContainer.parentNode.removeChild(messageContainer);
        }, 2000);
    };

    // Remove on click or after 10 sec.
    messageContainer.addEvent("click", remove);
    window.setTimeout(remove, 10000);

    return messageContainer;
};

/**
 * An ErrorView. The input is a JSON object, generated by error.json
 * @param   {object} error the error object
 * @returns {string} errorPacketHandler object
 */
var errorPacketHandler = function (error) {
    // Verstecken der Seite, solange ein Fehler angezeigt wird
    if (!getId('__contentwrapper')) {
        return debug(error);
    }

    getId('__contentwrapper').addClassName('hideFromView');
    (new LoadingView()).hide();

    this.error = error;
    this.error.errorMessage = this.error.errorMessage ? (this.error.errorMessage.message || this.error.errorMessage) : getTranslation("errorPacketHandler.noContentProvided");

    var supportEmail = this.error.supportEmail;
    this.error.supportEmail = null;

    if (supportEmail != null && supportEmail.trim().length > 0) {

        var mailURI = supportEmail.parseUri();

        if (!mailURI.queryKey.subject) {
            mailURI.queryKey.subject = "";
        }

        if (!mailURI.queryKey.body) {
            mailURI.queryKey.body = getTranslation("errorPacketHandler.defaultEmail.body");
        }

        var subjectMessage = encodeURIComponent(this.error.errorMessage.replace(new RegExp("[\r\n\t]", 'g'), " ").replace(new RegExp("\\s+", 'g'), " "));
        var maxSubjectLength = 60;
        if (subjectMessage.length > maxSubjectLength) {
            subjectMessage = subjectMessage.substr(0, maxSubjectLength - 3) + '...';
        }

        mailURI.queryKey.subject += " " + subjectMessage;
        mailURI.queryKey.body += "\r\n\r\n" + this.error.errorMessage + "\r\n";
        supportEmail = mailURI.user + '@' + mailURI.host + '?subject=' + mailURI.queryKey.subject.trim() + '&body=' + encodeURIComponent(mailURI.queryKey.body);
    }

    var errorViewWrapper = new popupHandler(null, function() {
        getId('__contentwrapper').removeClassName('hideFromView');
    }, '__errorViewWrapper', true).show();
    errorViewWrapper.addHeader(this.error.errorHeader || getTranslation("errorPacketHandler.anErrorOccurred") + " (" + this.error.errorCode + ")");

    var errorMessage = document.createElement( this.error.errorMessage.indexOf( "<div" ) > 0 ? "div" : "p" );
    errorMessage.innerHTML = this.error.errorMessage;
    errorViewWrapper.addBody( errorMessage );

    // Reset
    this.error.errorMessage = null;
    this.error.errorHeader = null;

    for (var item in this.error) {
        let value = this.error[item];
        if (!value || value == null) {
            continue;
        }

        item = item.replace(
            new RegExp("(^[a-z]|[A-Z])", "g"),
            function ($1) {
                return " " + $1.toUpperCase();
            });

        errorViewWrapper.addDetail(item, value);

        if (supportEmail != null && supportEmail.trim().length > 0) {
            supportEmail += encodeURIComponent("\n" + item + " :" + value);
        }
    }

    if (supportEmail != null && supportEmail.trim().length > 0) {
        var supportButton = document.createElement("a");
        supportButton.className = "supportEmail";
        supportButton.appendChild(document.createTextNode(getTranslation("errorPacketHandler.requestSupport")));

        supportEmail = "mailto:" + supportEmail;
        var maxLength = 2000;
        if (supportEmail.length > maxLength) { // Physical length for certain systems, like Windows.

            // remove trailing single unicode char
            supportEmail = supportEmail.substr(0, maxLength - 3).replace(new RegExp("%.?$"), "") + "...";
        }

        supportButton.href = supportEmail;

        errorViewWrapper.addBody(supportButton);
    }
    
    // Input with error data to send to the website
    var supportForm = document.createElement("form");
    supportForm.setAttribute( "method", "post" );
    supportForm.setAttribute( "action", "https://www.inetsoftware.de/external-services/error/" );
    
    var submitButton = document.createElement("input");
    submitButton.className = "supportEmail";
    submitButton.setAttribute( "type", "submit" );
    submitButton.setAttribute( "value", getTranslation("errorPacketHandler.requestSupportFromWebsite") );
    supportForm.appendChild(submitButton);
    
    var errorKeys = Object.keys( this.error );
    for( var i=0; i<errorKeys.length; i++ ) {
        let key = errorKeys[i];
        let value = this.error[key];
        
        if ( !key || !value ) {
            continue;
        }

        switch( key.toLowerCase() ) {
            case "errorcode": key = "errnumber"; break;
            case "stacktrace": key = "stacktrace"; break;
            case "serverversion": key = "version"; break;
            case "javaversion": key = "server_java_version"; break;
        }

        var valueInput = document.createElement("input");
        valueInput.setAttribute( "type", "hidden" );
        valueInput.setAttribute( "name", key );
        valueInput.setAttribute( "value", value );
        supportForm.appendChild(valueInput);
    }
    
    errorViewWrapper.addBody(supportForm);
};

var loglevel = false;
if (!window.console) {
    /* jshint -W020 */
    console = {};
    console.log = function () {};
    /* jshint +W020 */
}
/**
 * Debugging. Send a message to the console
 * you have to set "loglevel = true" in the console prior to seeing output.
 * 
 * @param {string} message error message
 */
var debug = function (message) {
    if (loglevel) {

        if (typeof printStackTrace != 'undefined') {
            var stack = printStackTrace().reverse();
            stack.splice(-4, 4);
            console.log(stack.join("\n >") + "\n >>" + message);
        } else {
            console.log(message);
        }
    }
};

var LoadingView_ = null;
/**
 * The LoadingView. Will count internally how often it was called.
 * You have to close them synchronously
 * 
 * @class
 * @example (new LoadingView()).show()
 * @example (new LoadingView()).hide()
 * @returns {object} LoadingView
 */
var LoadingView = function () {
    if (LoadingView_ != null) {
        return LoadingView_;
    }

    var self = this;
    LoadingView_ = self;
    this.isLoading = null;
    this.loadingSubText = null;
    this.showCount = 0;

    /**
     * Show the loading view
     * @param {string} extraText Optional text that should be displayed
     */
    this.show = function (extraText) {

        if (getId('__loadingView')) {
            self.showCount++;
            debug("LoadingView ShowCount increased to: " + this.showCount);

            if (extraText) {
                setInnerText(this.loadingSubText, extraText);
            }

            if (this.isLoading != null) {
                // Allready in progress
                return;
            }

            this.isLoading = setTimeout(function () {
                debug("Showing LoadingView");
                getId('__loadingView').setIsCurrent(true);
                self.isLoading = null;
            }, 500);
            return;
        }

        debug("Constructing LoadingView");

        // Create basic view
        var view = document.createElement('div');
        view.id = '__loadingView';

        var background = document.createElement('div');
        background.className = '__loadingViewBackground';
        view.appendChild(background);

        var contentwrapper = document.createElement('div');
        contentwrapper.className = '__loadingViewContentWrapper';
        view.appendChild(contentwrapper);

        var content = document.createElement('div');
        content.className = '__loadingViewContent';
        contentwrapper.appendChild(content);

        var loadingImage = document.createElement('div');
        loadingImage.className = '__loadingViewImage';
        content.appendChild(loadingImage);

        var loadingText = document.createElement('span');
        loadingText.appendChild(document.createTextNode(getTranslation("loadingView.loading")));
        content.appendChild(loadingText);

        this.loadingSubText = document.createElement('small');
        content.appendChild(this.loadingSubText);

        if (extraText) {
            setInnerText(this.loadingSubText, extraText);
        }

        var node = window.jQuery ? jQuery('div.webguicontainer')[0] : getId('__contentwrapper').parentNode;
        node.appendChild(view);
        this.show();
    };

    /**
     * Hide the loading view. Or at least count down when multiple views have been called.
     */
    this.hide = function () {
        if (--this.showCount <= 0) {
            if (this.isLoading != null) {
                clearTimeout(this.isLoading);
                this.isLoading = null;
            }

            if (getId('__loadingView')) {
                debug("Hiding LoadingView");
                getId('__loadingView').setIsCurrent(false);
                setInnerText(this.loadingSubText, '');
            }
        }

        // Sanity
        if (this.showCount < 0) {
            this.showCount = 0;
        }

        debug("LoadingView ShowCount decreased to: " + this.showCount);
    };
};

/**
 * Loading with delay for menubar icons
 */
var menubarLoading = (function () {

    var loadingButtons = [];
    return {
        /**
         * Start the loading indicator with 500ms delay
         * @param {string} button the button ID to set loading
         */
        start: function (button, disableButton) {

            if (loadingButtons[button]) {
                // already running
                return;
            }

            debug("Starting menubarLoading on: " + button);
            loadingButtons[button] = setTimeout(function () {
                debug("menubarLoading on: '" + button + "' has started now.");
                var btn = getId(button);
				btn && btn.addClassName('loading');
				btn && disableButton && btn.setEnabled( false );
            }, 1000);
        },

        /**
         * Stop the loading indicator and remove its timeout
         * @param {string} button the button ID to stop loading
         */
        stop: function (button, disableButton) {

            debug("Stoping menubarLoading on: " + button);
            if (loadingButtons[button]) {
                clearTimeout(loadingButtons[button]);
                delete loadingButtons[button];
                debug("Cleared the timeout on: " + button);
            }

            var btn = getId(button);
			btn && btn.removeClassName('loading');
			btn && disableButton && btn.setEnabled( true );
        }
    };
})();

/*******************************************************************************
 * A Combobox that allows editing the content
 ******************************************************************************/
var editableComboBox = function (name, className) {
    var self = this;
    this.combobox = null;
    this.input = null;
    this.selectButton = null;
    this.selectPane = null;

    // To be implemented by parent
    this.updateAction = function (_selectedValue, _event) {};

    // Update the current Status
    this.updateInputValue = function (selectedValue) {
        debug("UPDATE VALUE: " + self.input.name + " => " + selectedValue);
        self.input.value = selectedValue;
    };

    // Select a value from the options list
    this.selectValue = function ( event ) {
        if (typeof this.lastChild != 'object' || !this.lastChild.value) {
            return;
        }
        self.updateAction(this.lastChild.value, event);
    };

    // Set value from input - on blur or Enter - to be implemented when needed
    this.setValueFromInput = function ( event ) {
        // self.input.select(); // this would re-select the field.

        // unwanted event. The value has not changed
        if ( self.input.getAttribute('_value') == self.input.value ) { return; }
        self.updateAction(self.input.value, event);
    };

    // create a new option in the list
    this.addOption = function (value, postString) {

        this.selectButton.addClassName('visible');
        var option = document.createElement("li");
        option.appendChild(document.createTextNode(value.toString() + postString));

        var optionValue = document.createElement("input");
        optionValue.setAttribute('value', value);
        optionValue.type = 'hidden';
        optionValue.name = 'optionValue_' + value;
        option.appendChild(optionValue);

        option.addEvent('click', self.selectValue);
        this.selectPane.appendChild(option);
    };

    // Set visibility of the selection pane
    this.toggleSelectPane = function (e) {

        KEYBOARD_LISTENER.stopEvent(e);
        if (self.selectPane.style.display === '' || self.selectPane.style.display == 'none') {
            self.selectPane.style.display = 'block';
            document.documentElement.addEvent('click', self.toggleSelectPane);
        } else {
            self.selectPane.style.display = 'none';
            document.documentElement.removeEvent('click', self.toggleSelectPane);
        }

        self.focusInput(e);
    };

    // focus the input field of the dropdown
    this.focusInput = function () {

        try {
            var input = this.input || this;
            input.setAttribute('_value', this.value);
            input.select();
        } catch (e) {}
    };

    // init the combobox
    this.init = function (name, className) {
        this.combobox = document.createElement("div");
        this.combobox.className = "__comboBox";

        this.input = document.createElement("input");
        this.input.className = "__comboInput";
        this.input.name = "__comboInput_" + name;

        this.selectButton = document.createElement("div");
        this.selectButton.addClassName("__comboButton");
        this.selectButton.addClassName(className);

        this.selectButton.appendChild(document.createElement("i"));

        this.selectPane = document.createElement("ul");
        this.selectPane.className = "__comboList";

        this.combobox.appendChild(this.input);
        this.combobox.appendChild(this.selectButton);
        this.combobox.appendChild(this.selectPane);

        this.selectButton.addEvent('click', self.toggleSelectPane);
        this.input.addEvent('blur', self.setValueFromInput);
        this.input.addEvent('focus', self.focusInput);
        this.input.addEvent('keydown', KEYBOARD_LISTENER.checkDownKey);
    };

    this.init(name, className || "");
};

/**
 * Scroll an element into the center of the screen
 * @param {object} element the element
 */
var scrollToElement = function (element, _event) {

    var zoom = (new MenubarActions()).currentZoom;
    var topElement = getId('__contentwrapper', true);
    var absoluteTop = element.absoluteOffsetTop(); // This is really absolute by 100% zoom.
    var height = element.parentNode.offsetHeight;
    var centerOffset = Math.max(50, (windowSize().height - height) / 2);

    var scrollTop = topElement.scrollTop;
    topElement.scrollTop = absoluteTop - centerOffset;
    debug("zoom: " + zoom + " topElement.absolutetop: " + absoluteTop + " element.height: " + height + " centerOffset: " + centerOffset + " " + "previous scrolltop: " + scrollTop + " final scrollTop: " + topElement.scrollTop);
    if ( loglevel ) { highlightElements( element, false); }
};

/**
 * Highlight elements with an animated box arround it
 * @param {object}  listOfFadingElements List of elements (or a single one) to be highlighted
 * @param {boolean} putInside            should the fading view be put inside if the elements or arround
 */
var highlightElements = function (listOfFadingElements, putInside) {

    if (!listOfFadingElements.isArray || !listOfFadingElements.isArray()) {
        listOfFadingElements = [listOfFadingElements];
    }

    var listOfFading = [];
    for( var i=0; i<listOfFadingElements.length; i++ ) {
        var elem = listOfFadingElements[i];
        var fading = document.createElement("div");
        fading.className = "fading";

        if (putInside) {
            elem.appendChild(fading);
        } else {
            elem.parentNode.appendChild(fading);
        }

        listOfFading.push(fading);
    }

    var content = getId((new MenubarActions()).getCurrentPageName(), true);

    // Fading auf Content, für overflow visible;,
    window.setTimeout(function () {
        listOfFading.forEach(function (el) {
            el.addClassName('fading_animation');
            el.parentNode.style.position = "relative";
        });

        content.addClassName('fading_animation');
//*
        window.setTimeout(function () {
            listOfFading.forEach(function (el) {
                el.removeClassName('fading_animation');
            });
            window.setTimeout(function () {
                content.removeClassName('fading_animation');
                listOfFading.forEach(function (el) {
                    if (el && el.parentNode) {
                        el.parentNode.removeChild(el);
                    }
                });

            }, 1000);
        }, 1000);
//*/
    }, 100);
};

/*******************************************************************************
 * Browser Language Support
 ******************************************************************************/
function getBrowserLanguage() {
    if (navigator) {
        if (navigator.language) {
            return navigator.language;
        } else if (navigator.browserLanguage) {
            return navigator.browserLanguage;
        } else if (navigator.systemLanguage) {
            return navigator.systemLanguage;
        } else if (navigator.userLanguage) {
            return navigator.userLanguage;
        }
    }

    return null;
}

/**
 * Set an inner text of an element
 * @param {object} elem the element
 * @param {string} text the text
 */
var setInnerText = function (elem, text) {
    if (document.all) {
        elem.innerText = text;
    } else {
        elem.textContent = text;
    }
};

/**
 * Calculate and return the windows size
 * @returns {object} object with {height, width}
 */
var windowSize = function () {
    var winW = 630,
        winH = 460;
    if (document.body && document.body.offsetWidth) {
        winW = document.body.offsetWidth;
        winH = document.body.offsetHeight;
    }
    if (document.compatMode == 'CSS1Compat' && document.documentElement && document.documentElement.offsetWidth) {
        winW = document.documentElement.offsetWidth;
        winH = document.documentElement.offsetHeight;
    }
    if (window.innerWidth && window.innerHeight) {
        winW = window.innerWidth;
        winH = window.innerHeight;
    }

    return {
        height: winH,
        width: winW
    };
};

Object.extend = function (destination, source) {
    for (var property in source) {
        if (source.hasOwnProperty(property)) {
            destination[source[property].name] = source[property].value;
        }
    }
    return destination;
};

/**
 * Smooth out (resizing) events
 * 
 * @param {function} callback Callback        after event is really done
 * @param {number}   ms       how             long to wait in ms
 * @param {string}   uniqueId ID of the event.
 */
var waitForFinalEvent = (function () {
    var timers = {};
    return function (callback, ms, uniqueId) {

        if (!uniqueId) {
            uniqueId = "Don't call this twice without a uniqueId";
        }

        return function (e) {
            clearTimeout(timers[uniqueId]);
            timers[uniqueId] = setTimeout(callback, ms, e);
        };
    };
})();

/**
 * Tries to get the time zone key directly from the operating system for those
 * environments that support the ECMAScript Internationalization API.
 * 
 * @returns {string} the timezone
 */
var timezoneFromInternationalizationAPI = function () {
    if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat === "undefined") {
        return '';
    }
    var format = Intl.DateTimeFormat();
    if (typeof format === "undefined" || typeof format.resolvedOptions === "undefined") {
        return '';
    }
    return format.resolvedOptions().timeZone || '';
};

/**
 * Iterate over form fields and apply a function on them
 * @param {object}   elements list of elements
 * @param {function} func     function to apply
 */
var applyFormFieldFunction = function (elements, func) {

    if (!elements || typeof elements != 'object' || !elements.forEach) {
        return;
    }

    elements.forEach(function (elem, i) {

        if (new Array('checkbox', 'radio').in_array(elem.type) && !(elem.checked || elem.selected)) {
            return;
        }

        func(elem, i);
    });
};