Source: script.js

"use strict";
/* global LoadingView */
/* global MenubarActions */
/* global SACK */
/* global XRegExp */
/* global addPromptToAjax */
/* global addPromptToObject */
/* global amIOnline */
/* global applyFormFieldFunction */
/* global dataAsJson */
/* global dataHasJSONError */
/* global debug */
/* global dropDownMessage */
/* global errorPacketHandler */
/* global escape */
/* global getCacheKey */
/* global getId */
/* global getPageCount */
/* global getPromptsField */
/* global getTranslation */
/* global handleErrorPage */
/* global keepServerCacheAlive */
/* global keylistener */
/* global loglevel */
/* global menubar */
/* global menubarLoading */
/* global popupHandler */
/* global preg_replace_callback */
/* global promptHandling */
/* global setPromptsField */
/* global tabbedPanel */
/* global timezoneFromInternationalizationAPI */

/** Global variable to maintain the cache over the report render session */
var vgen = null;
var i18n = {};

/**
 * Add additional translations.
 * @param translations the new translations to add
 */
/* jshint -W098 */
function addI18n( translations ) {
/* jshint +W098 */
    Object.keys( translations ).forEach(function(key) {
       i18n[key] = translations[key];
    });
}

/**
 * Merge URL parameters into array of existing URL parameters
 * The function is intended to prepare the URL parameters that will be send to the server
 * 
 * @param   {Array}   array            Array to merge the new parameters into
 * @param   {Array}   additionalParams Array of parameters to be merged
 * @param   {boolean} encode           If the parameters shoud be encodeUROComponent encoded
 * @returns {Array}   Array of merged paramters
 */
function prepareAddParameterToArray(array, additionalParams, encode) {
    for (var param in additionalParams) {
        if (additionalParams[param] === null) {
            continue;
        }
        // Allow additional encoding for special cases like export.
        // subreport* is allways encoded and must never be recoded. ever.
        // Also check if already encoded.
        if (encode && param != 'subreport_ondemand' && param != 'subreport' && !( alreadyEncoded(param) || alreadyEncoded(additionalParams[param]) ) ) {
            array[encodeURIComponent(param)] = encodeURIComponent(additionalParams[param]);
        } else {
            array[ensureParameterPromptKey(param)] = additionalParams[param];
        }
    }

    return array;
}

/**
 * Check if a string is already url encoded
 * @param input
 * @returns true if already encoded
 */
function alreadyEncoded( input ) {
    if ( input != null && input.indexOf && input.indexOf('%') == -1  ) {
        return false; // Can't be encoded
    }
    try {
        if ( decodeURIComponent( input ) == input ) {
            return false; // Gibberish
        }
    } catch (e) {
        // Ok, there is a percent sign but it is not encoded ....
        return false;
    }
    return true;
}

/**
 * Remove parameters from the Array.
 * 
 * @param   {Array} array            Input Array
 * @param   {Array} additionalParams List of names to be removed from the input array
 * @returns {Array} Array with removed entries
 */
/* jshint -W098 */
function prepareRemoveParameterFromArray(array, additionalParams) {
/* jshint +W098 */
    for (var param in additionalParams) {
        delete(array[param]); // Delete standard param
        delete(array[encodeURIComponent(param)]); // delete encoded param of same kind
    }

    return array;
}

/**
 * Prepare additional parameters and encode if needed.
 * Will also generte a {@link vgen} if not set already
 * Uses {@link prepareaddParameterToArray}
 * 
 * @param   {Array}   additionalParams parameters to put together
 * @param   {boolean} encode           if it should encode
 * @returns {Array}   Array of parameters
 */
function prepareAddParameter(additionalParams, encode) {

    if (vgen == null) {
        vgen = new Date().getTime();
    }

    // 2018-08-07 - the variables by contain prompts that are not yet encoded. That is why we need to take care.
    var preparedVariable = prepareAddParameterToArray({vgen: vgen}, VARIABLES, encode);
    return prepareAddParameterToArray(preparedVariable, additionalParams, encode);
}

/**
 * Uses {@link prepareAddParameter} to set all variables to the AJAX request
 * 
 * @param {object} ajax             the AJAX Request
 * @param {Array}  additionalParams the parameters
 */
function addParametersToAJAX(ajax, additionalParams) {
    var newVars = prepareAddParameter(additionalParams);
    for (var param in newVars) {
        ajax.setVar( ensureParameterPromptKey( param ), newVars[param]);
    }
}

/**
 * Ensure that the default zoom has a reasonable value
 * @oaram {object} the inout value
 * @returns a curated zoom value
 */
function ensureDefaultZoom(zoom) {
    switch (zoom) {
    case getTranslation("menuBar.zoom.pageWidth"):
    case getTranslation("menuBar.zoom.pageHeight"):
    case getTranslation("menuBar.zoom.pageFit"):
        return zoom;
    }

    var match = (zoom + '').match(new RegExp("^([0-9]+|([0-9]+)%*)$", "i"));
    if (!match || !match[1]) {
        return 100;
    }
    return parseInt(match[2] ? match[2] : match[1]) || 100; // Reset zoom, if not able to parse
}

/**
 * Prepares the parameter statement to be send to the server.
 * It will put all the additionParams together, add the PROMT parameters
 * and return a string of them
 * 
 * @param   {Array}   additionalParams		the parameters
 * @param   {boolean} [keepPrevious=false]	if previously set parameters should be kept
 * @returns {string}  the combined list of additionalParams and PROMPTS
 */
function buildParameterString(additionalParams, keepPrevious) {
    var newVars = prepareAddParameter(additionalParams);

    // do not encode here or it will be duplicate. See #28290
    // If encode is not set, do encode - or bad things happen.
    // seriously: when the prompt parameters are not encoded,
    // the can contain '#' which will be interpreted as anchor later on.
    // We not want.
    if ( addPromptToObject ) { addPromptToObject(newVars, keepPrevious); }

    var returnString = [];

    // Iterate over the keys of the object and store them in an array
    var keys = Object.keys(newVars);

    // Sort the keys alphabetically
    keys.sort();

    // Iterate over the sorted keys and push the corresponding key-value pairs into the returnString array
    for (var i = 0; i < keys.length; i++) {
        var param = keys[i];
        returnString.push(ensureParameterPromptKey(param) + '=' + newVars[param]);
    }

    return returnString.join("&");
}

/**
 * Prompts for subreports may contain paramters that need special treatment.
 * @param name a parameter name with a potential #-sign
 * @returns if the parameter name included a #-sign, encoded vbalue
 */
function ensureParameterPromptKey( name ) {
    return name.indexOf('#') >= 0 ? encodeURIComponent( name ) : name;
}

/**
 * Build the page URL.
 * @param   {string}  [page='']        		the page to open, can be number or string if it is a subreport
 * @param   {boolean} isPOST           		send via POST?
 * @param   {object}  additionalParams		list of additional parameters
 * @param   {boolean} [keepPrevious=false]	if previously set parameters should be kept
 * @returns {string}  The full URL to request a report file
 */
function buildBASEURLForPageFile(page, isPOST, additionalParams, keepPrevious) {
    // add a page to the parameters
    if (typeof additionalParams == 'undefined') {
        additionalParams = {};
    }

    page = page || '';

    // If a page is set, check for its parameters, because it could be a subreport I think
    if (page.length > 0 && page.indexOf('?') >= 0) {
        // var additionalFolder = '';
        var params = page.substr(page.indexOf('?')).parseUri().queryKey;
        additionalParams = prepareAddParameterToArray(additionalParams, params);
        page = page.substr(0, page.indexOf('?'));
    }

    // If still set, set.
    if (page.length > 0) {
        additionalParams.page = page;
    }

    // 2015-02-16: Add a / only if it is not already there and we want to add a page.
    return BASE + (!amIOnline.isOnline ? (BASE[BASE.length - 1] == '/' ? '' : '/') + page : '') + (isPOST === true ? '' : '?' + buildParameterString(additionalParams, keepPrevious));
}

/**
 * Page Caching machanism
 * @class
 */
var pageCache = function () {

    var self = this;
    this.cachedPages = {};
    this.menu = new MenubarActions();

    this.notAllowedParameters = ['vgen'];
    this.notAllowedCacheParameterToSave = this.notAllowedParameters.concat(['cmd']);

    /**
     * Return a cache key for a given pageAJAX
     * 
     * @param   {object} pageAJAX    the current ajax request for a page
     * @param   {object} filterArray an optional array of parameters not to take into account
     * @returns {string} cache key for the current page
     */
    this.getCacheKey = function (pageAJAX, filterArray) {

        if (!filterArray) { filterArray = self.notAllowedCacheParameterToSave; }
        var key = pageAJAX.requestFile + '?' + pageAJAX.parameterString(function (value, index, originalArray) {
            return !filterArray.in_array(value);
        });

        try {
            if (loglevel) {
                var debugOutput;
                switch (getCacheKey.caller) {
                case this.getPage:
                    debugOutput = "getPage";
                    break;
                case this.setPage:
                    debugOutput = "setPage";
                    break;
                case this.isPageLoading:
                    debugOutput = "isPageLoading";
                    break;
                case this.setPageLoading:
                    debugOutput = "setPageLoading";
                    break;
                }

                debug("Cache Key for " + debugOutput + "(): '" + key);
            }
        } catch (e) {}

        return key;
    };

    /**
     * Return a cached page if it has already been loaded
     * 
     * @param   {object} pageAJAX the prepared ajax request of the page
     * @returns {object} the cache object of the page
     */
    this.getPage = function (pageAJAX) {
        if (!this.isPageLoading(pageAJAX)) {
            return this.cachedPages[this.getCacheKey(pageAJAX, this.notAllowedParameters)];
        }
    };

    /**
     * Set a loaded page for a given ajax request
     * 
     * @param   {object} pageAJAX the prepared ajax request for the page
     * @param   {object} page     the page (full HTML) of the request
     * @returns {object} the page again
     */
    this.setPage = function (pageAJAX, page) {
        this.cachedPages[this.getCacheKey(pageAJAX)] = page;
        return page;
    };

    /**
     * Sets the status of a page to 'loading'
     * 
     * @param {object} pageAJAX the prepared ajax request for the page
     */
    this.setPageLoading = function (pageAJAX) {
        this.cachedPages[this.getCacheKey(pageAJAX)] = 'loading';
    };

    /**
     * checks if a page status uns 'loading'
     * 
     * @param   {object}  pageAJAX the prepared ajax request for the page
     * @returns {boolean} true if loading
     */
    this.isPageLoading = function (pageAJAX) {
        return this.cachedPages[this.getCacheKey(pageAJAX)] === 'loading';
    };

    /**
     * Create a new page object that can be set as a cache object
     * 
     * @param   {string} pageNr    the number of the page or subreport page
     * @param   {string} body      the full, rendered body of the page
     * @param   {string} pageStyle the full, rendered style of the page
     * @returns {object} a page
     */
    this.page = function (pageNr, body, pageStyle) {

        this.pageBody = body;
        this.pageNr = pageNr;
        this.css = pageStyle;
        this.width = null;
        this.height = null;

        /**
         * Stripped down version of the CSS
         * @returns {string} CSS of the page
         */
        this.pageStyle = function () {
            return this.css.replace(new RegExp('([{}](\r|\n)*?)([^$}@,\r\n]*?\s?({|,)((?!;).)*?)(\r|\n)?', 'gi'), "$1#" + this.pageCurrent() + " $3$6");
        };

        /**
         * The unique ID of the page style, prefixed
         * @returns {string} the ID of the page css
         */
        this.pageCSSID = function () {
            return self.menu.getCurrentPageName(getId('endless-mode').isCurrent() ? this.pageNr : null, '__pageStyles');
        };

        /**
         * returns the page number if it is the current page.
         * @returns {string} page number
         */
        this.pageCurrent = function () {
            return self.menu.getCurrentPageName(getId('endless-mode').isCurrent() ? this.pageNr : null);
        };
    };
};
var _pageCache = new pageCache();

/**
 * PAGE LOADING. Here it will really start a request or return a cached page
 * 
 * @class
 * @param   {string}   pageNr           the page number or subreport number to load
 * @param   {function} onloadEvent      a function to call when the report page has finished
 * @param   {string}   fragment         an anchor fragment to use
 * @param   {boolean}  dontStartJustNow true if the page request should start loading whan constructed
 * @returns {object}   page loading object
 */
/* jshint -W098 */
var loadpage = function (pageNr, onloadEvent, fragment, dontStartJustNow) {
/* jshint +W098 */

    var self = this;
    this.fragment = fragment;
    this.pageURL = buildBASEURLForPageFile(pageNr + '.html', true);

    this.wasError = false;
    this.ajaxCanHandleErrors = true;
    this.ajax = new SACK(this.pageURL);
    // this.ajax.encodeURIString = true;
    this.onLoadEvent = onloadEvent;
    this.pageFinished = null;
    this.pageNr = pageNr;
    this.loadingDone = false;
    this.scrollBeforeLoad = {
        left: 0,
        top: 0
    };

    this.restartLoadingTimeout = 5; // seconds 

    this.pageNotFoundErrorHandler = null;
    this.pageNotfoundError = {
        errorHeader: getTranslation("loadPage.pageNotFound.subject"),
        errorMessage: getTranslation("loadPage.pageNotFound.body")
    };

    this.responseEvaluationError = {
        errorHeader: getTranslation("loadPage.responseError.subject"),
        errorMessage: getTranslation("loadPage.responseError.body")
    };

    /**
     * Cancel loading of the AJAX request
     */
    this.cancelLoading = function () {
        try {
            if (this.ajax.abort) {
                this.ajax.abort();
            }
        } catch (e) {}
        (new LoadingView()).hide();
    };

    this.ajax.onCompletion = function () {

        /* check for error */
        var data = self.ajax.response;
        var status = self.ajax.responseStatus[0];

        delete(VARIABLES.promptonrefresh); // Whatever: this must go.

        if (status == 404 && self.pageNr != 1) { // Report is rendered, but page is not available.
            return getPageCount.supplyWithData(function () {
                (new MenubarActions()).lastPage();
            });
        } else if (dataHasJSONError(data, self.ajax.xmlhttp)) {

            // 2016-10-04 - there is a new indicator for that. It might not reset on errors
            menubarLoading.stop('reload');
            return;
        }

        if (status >= 200 && status < 400) {
            if (typeof data == "string" && data !== '') {

                if (data.match(new RegExp("div class=\"promptdialog\"", "ig"))) {
                    // Build FULL URL again
                    var promptDialog = new promptHandling(self.ajax.requestFile + (self.ajax.requestFile.indexOf("?") != -1 ? "&" : "?") + self.ajax.parameterString(null, false), self.onLoadEvent);
                    promptDialog.popup.show();
                    _pageCache.setPage(self.ajax, null); // Reset page loading status
                    return;
                } else if (!data.match(new RegExp("<meta name=\"generator\" content=\"i-net Clear Reports\" />"))) {
                    /*
                     * new errorPacketHandler({ errorHeader: 'Did not receive
                     * response as expected', errorMessage: data });
                     */

                    if (status == 200) {
                        // Received a page, maybe a login form?
                        var origRequestURL = self.ajax.requestFile;
                        var origParameters = self.ajax.parameters;
                        (new popupHandler(origRequestURL + (origRequestURL.indexOf("?") != -1 ? "&" : "?") + self.ajax.parameterString(null, false), function (event, form, input, action) {

                            self.ajax.requestFile = this.formAction;
                            self.ajax.createAJAX();
                            self.ajax.parameters = [];
                            applyFormFieldFunction(input, function (elem, i) {
                                self.ajax.setVar( ensureParameterPromptKey(elem.name), elem.value );
                            });

                            var onLoad = self.ajax.onCompletion;
                            self.ajax.onCompletion = function () {
                                self.ajax.requestFile = origRequestURL;
                                self.ajax.parameters = origParameters;
                                if (typeof onLoad == 'function') {
                                    onLoad();
                                }
                            };

                            try {
                                (new LoadingView()).show();
                                self.ajax.runAJAX();
                            } catch (e) {
                                (new LoadingView()).hide();
                                debug(e);
                            } finally {
                                this.hide();
                            }

                            return true;
                        })).show();
                        _pageCache.setPage(self.ajax, null); // Reset page loading status
                        var authHeader = decodeURIComponent((self.ajax.xmlhttp.getResponseHeader('X-Authentication-Message')||'').replace(new RegExp("\\+", "g"), '%20'));
                        if (authHeader && authHeader != '') {
                            new dropDownMessage(authHeader);
                        }

                    } else {
                        self.restartLoading();
                    }
                    return;
                } else {
                    delete(VARIABLES.cmd);
                }

                // look for body, remove onload method and create div out of it
                var pageStyle = data.replace(new XRegExp('^.*?<style.*?>(.*?)<\/style.*?$', 'sgi'), "$1");
                var permissions = dataAsJson(data.replace(new XRegExp('^.*?var permissions=(\{.*?\});.*?$', 'sgi'), "$1"));
                self.addStylesAndBodyToDom(pageStyle, data, permissions);

            } else {
                // Try again
                setTimeout(self.restartLoading, self.restartLoadingTimeout * 1000);
            }
        } else if (!self.ajax.canceled && status > 0) {
            if ( keepServerCacheAlive.isReportFinished() ) {
                handleErrorPage(data, status);
                return;
            }

            self.restartLoading();
        }
    };

    this.addStylesAndBodyToDom = function (pageStyle, body, permissions) {

        var menu = new MenubarActions();

        var pageNr = getId('endless-mode').isCurrent() ? self.pageNr : null;
        var currentPageName = menu.getCurrentPageName(pageNr);

        pageStyle = pageStyle.replace(new RegExp('body', 'ig'), "div.htmlviewer_body");

        body = body.replace(new XRegExp('^.*?<body(.*?)class="(.*?)"(.*?)(onload=".*?")?(.*?<\/)body.*?$', 'si'), "<div class='htmlviewer_body $2'$1$3$5div>");

        body = preg_replace_callback('/(<[^>]*?)(href|src|action)="(?!data:)([^">]*?)"([^>]*?>)/ig', self.linkReplacer, body);

		// Note: this may not work with multiple URLs in the same styl
        body = preg_replace_callback('/(<[^>]*?style="[^">]*?:url\\()(?!data:)([^;">]*?)(\\)[;">])/ig', self.inlineCssLinkReplacer, body);

        // Now do the stuff!
        if (!getId(currentPageName)) {
            // This is terrible!!!
            alert(getTranslation("loadpage.unexpectedError"));
            return;
        }

        self.setPageContent(_pageCache.setPage(self.ajax, new _pageCache.page(self.pageNr, body, pageStyle)));

        menu.updatePermissions(permissions);
    };

    /**
     * Set a page content to the current tab
     * @param   {object}  page the page from the cache
     * @returns {boolean} true if successfull
     */
    this.setPageContent = function (page) {
        if (!page) {
            return false;
        }

        var css = getId(page.pageCSSID());
        if (!css) {
            css = document.createElement('style');
            css.setAttribute('type', 'text/css');
            css.setAttribute('media', 'screen,print');
            css.id = page.pageCSSID();
            document.getElementsByTagName("head")[0].appendChild(css);
        }

        if (css.styleSheet) { // IE does it this way
            css.styleSheet.cssText = page.pageStyle();
        } else { // everyone else does it this way
            css.innerHTML = ""; // Clear first
            css.appendChild(document.createTextNode(page.pageStyle()));
        }

        getId(page.pageCurrent()).innerHTML = page.pageBody;
        getId(page.pageCurrent()).htmlViewerPage = page; // Save page object to current node.
        self.finalizeLoadPage(page.pageCurrent(), page.pageCSSID(), page);
    };

    // Finally reset the Menu-things
    this.finalizeLoadPage = function (currentPageName, cssID, page) {

        var menu = new MenubarActions();
        if (currentPageName == null) {
            currentPageName = menu.getCurrentPageName();
        }

        if (self.pageNotFoundErrorHandler != null) {
            self.pageNotFoundErrorHandler.closeFunction();
            (new LoadingView()).show();
            self.pageNotFoundErrorHandler = null;
        }

        getId('__contentwrapper').style.display = 'block';

        if (typeof self.onLoadEvent == 'function') {
            self.onLoadEvent();
        }

        // after "has been load" stuff, so we do not need to take care in multi-load
        var width = null,
            height = null;
        if (cssID) {
            try {
                // regex = "@page(.|[\r\n])*?size:(.*?);";
                var regex = ".body.*?{[^}]*?width:([0-9]+)px;[^}]*?height:([0-9]+)px;";
                // Use IE styles as well.
                var matches = (getId(cssID).innerHTML || getId(cssID).styleSheet.cssText).match(new RegExp(regex));
                width = matches[1].trim();
                height = matches[2].trim();
            } catch (e) {}
        }

        if (page) {
            page.width = width;
            page.height = height;
        }

        // async get size that is set in the end - also: load the group tree!
        getPageCount.setSizeOfContent();

        // set prepared size for the time being.
        var content = getId(currentPageName);

        if ( width && height ) {
            /* content.style.width = width + 'px';
            content.style.height = height + 'px'; */
            getPageCount.preliminaryDATA = {page:{width: width, height: height}};
            getPageCount.updateSizeOfContent( getPageCount.preliminaryDATA );
        }

        // Font-Autoscaling
        var dom = content.nodeName == 'IFRAME' ? (content.contentWindow || content.contentDocument).document : content;
        if (dom) {
            try {
				var elements = [].slice.call(dom.getElementsByTagName('p'));
				var advancedHTML = typeof dom.querySelectorAll == 'function' ? [].slice.call(dom.querySelectorAll(".no-scaling p")) : [];
				elements = elements.filter( function(e) { return advancedHTML.indexOf( e ) == -1; } );
				
                this.fontAutoScaling(elements);
            } catch (e) {
                throw e;
            }
            // Check all Links in the new DOM for subreport and reorder links
            var links = dom.getElementsByTagName('a');
            for (var i = 0; i < links.length; i++) {
                var link = links[i];
                var linkURL = link.href.parseUri();

                // Check for subreport
                if (!(linkURL.queryKey.subreport_ondemand || linkURL.queryKey.reorder)) {
                    continue;
                }

                /*if (linkURL.queryKey.reorder) {
                    //	return;
                }*/

                link.addEvent('click', function (event) {
                    KEYBOARD_LISTENER.stopEvent(event);
                    var element = event.currentTarget || event.srcElement;
                    var uri = element.href.parseUri();

                    amIOnline.check(function (isOnline) {

                        // Only available in online mode.
                        if (!isOnline) {
                            return;
                        }

                        if (uri.queryKey.subreport_ondemand) {

                            // This is a subreport on demand, so remove the subreport
                            // variable, or there will be an exception on the server.
                            if (uri.queryKey.subreport) {
                                delete(uri.queryKey.subreport);
                            }

                            menu.openSubreport(uri);
                        } else if (uri.queryKey.reorder) {

                            var prompts = getPromptsField();
                            for (var i = 0; i < prompts.length; i++) {
                                if (prompts[i].name == "reorder") {
                                    prompts.splice(i, 1);
                                    break;
                                }
                            }

                            prompts.push({
                                "name": "reorder",
                                "value": uri.queryKey.reorder
                            });
                            setPromptsField(prompts);
                            menu.refreshReport(null);
                        }
                    });

                    return false;
                });
            }
        }

        // Has to be done at the one so that the IE9+, Edge gets a repaint event at the proper time
        menu.setSafeZoom( $.jStorage.get( 'menu.zoom' , DEFAULTZOOM ) );

        self.loadingDone = true;

        if (typeof self.pageFinished == 'function') {
            self.pageFinished(self);
        }

        // Ok Done. Hide.
        (new LoadingView()).hide();
    };

    /**
     * Convert pt to px. if the entry already had px, nothing will be done.
     * @param sizeInPtOrPx size in pt or px
     * @retun size in px if it was pt
     */
    this.ptToPx = function( sizeInPtOrPx ) {
        return sizeInPtOrPx.replace(new RegExp("([0-9]+)pt", "g"), function (match, group0) {
            return (parseInt(group0, 10) * 96 / 72) + "px";
        });
    };


    // Autoscaling of p-block-elements
    this.fontAutoScaling = function (textBlockElements) {
        // Find all div blocks arround the p-Blocks
        var divBlocks = [];
        for (var i = 0; i < textBlockElements.length; i++) {
            let element = textBlockElements[i];
            let block = element.parentNode;
            if (block.nodeName != 'DIV') {
                continue;
            }

            /* var rotated = block.className.match(new RegExp("rot(90|270)"));
            if ( rotated != null ) {
            	block.removeClassName(rotated[0]);
            	block.rotationClassName = rotated[0];
            } */

			// This is problematic for rotated glyphs
            element.style.lineHeight = element.elementStyle().lineHeight; // required to force (IE) to have the correct line-height or it is derived from the parent - and thus never changed
            element.fontSizeInPx = this.ptToPx( element.elementStyle().fontSize );
            element.fontSizeInPx = element.fontSizeInPx.substr(0, element.fontSizeInPx.length - 2);
            element.subFontSizeInitialised = false;

            if (divBlocks.indexOf(element.parentNode) == -1) {
                block.textBlocks = [];
                block.contentHeight = 0;
                divBlocks.push(element.parentNode);
                block.fontSizeInPx = element.fontSizeInPx;
            }

            element.lineHeight = this.getLineHeight(element);
            block.textBlocks.push(element);
            block.contentHeight += element.offsetHeight;

            // Adjust fontSizeInPx to the largest value for the Block
            if (element.fontSizeInPx > block.fontSizeInPx) {
                block.fontSizeInPx = element.fontSizeInPx;
            }
        }

        for (var blockNumber = 0; blockNumber < divBlocks.length; blockNumber++) {
            let block = divBlocks[blockNumber];

            // Check if there is a min-height set. If so we need to convert it
            // to a height setting
            var minHeight = block.elementStyle().height;
            var height = block.elementStyle().minHeight;

            minHeight = parseInt(minHeight.substring(0, minHeight.length - 2));
            height = parseInt(height.substring(0, height.length - 2));
            height = minHeight > height && height > 0 ? height : minHeight;

            if (height > 0) {
                block.style.height = height + 'px';
            }

            // there may be lots and lots of Ps in a div - each with a different
            // font size.
            var boxHeight = block.offsetHeight;
            if (boxHeight >= block.contentHeight) {
                // Everything is great!
                if (block.rotationClassName) {
                    block.addClassName(block.rotationClassName);
                }

                // ContentHeight matches boxheight, so we don't need to define it here.
                // fixes a small problem where both was equal and the text was cut off then.
                // 2014-09-20 - Do not set on auto, but remove it entirely.
                delete(block.style.height);
                continue;
            }

            // Set the reference of the font-size in PX
            block.style.fontSize = block.fontSizeInPx + 'px';
            // block.style.lineHeight = '1em'; // required to force (IE) to have the correct line-height or it is derived from the parent - and thus never changed 
            var newSizeAdjustment = 0; // Current adjustment from default value
            // in %
            var adjustmentStepSize = 1 / block.textBlocks.length;

            while (boxHeight < block.contentHeight) {
                newSizeAdjustment += adjustmentStepSize;
                block.contentHeight = 0;
                for (var textBlockNumber = 0; textBlockNumber < block.textBlocks.length; textBlockNumber++) {
                    var textBlock = block.textBlocks[textBlockNumber];

                    // Initialize the text-sub-elements on every p to have the
                    // correct font-size
                    if (!textBlock.subFontSizeInitialised) {
                        textBlock.subFontSizeInitialised = true;
                        var node = textBlock.firstChild;
                        while (node) {
                            if (node.elementStyle) {
                                var fontSizeInPx = this.ptToPx( node.elementStyle().fontSize );
                                fontSizeInPx = fontSizeInPx.substr(0, fontSizeInPx.length - 2);
                                node.style.fontSize = (fontSizeInPx == textBlock.fontSizeInPx) ? 'inherit' : (100 / textBlock.fontSizeInPx * fontSizeInPx) + '%';
                            }

                            node = node.nextSibling;
                        }

                        // Set the corrected fontSize in % in relation to the
                        // surrounding block
                        textBlock.fontSizeInPercent = (100 / block.fontSizeInPx * textBlock.fontSizeInPx);
                    }

                    textBlock.style.fontSize = (textBlock.fontSizeInPercent - newSizeAdjustment) + '%';
                    block.contentHeight += textBlock.offsetHeight;
                }

                // Minimum font-size of 50%;
                if (newSizeAdjustment > 50) {
                    break;
                }
            }

            if (block.rotationClassName) {
                block.addClassName(block.rotationClassName);
            }
        }
    };

    this.lineHeightCache = {};
    this.getLineHeight = function (element) {
        var family = element.elementStyle().fontFamily;
        var size = element.elementStyle().fontSize;

        if (typeof this.lineHeightCache[family] == 'undefined') {
            // Init font
            this.lineHeightCache[family] = {};
        }

        // Check on Fontsize
        if (typeof this.lineHeightCache[family][size] == 'undefined') {
            var temp = document.createElement(element.nodeName);
            temp.setAttribute("style", "margin:0px;padding:0px;font-family:" + family + ";font-size:" + element.elementStyle().fontSize);
            temp.innerHTML = "test";
            temp = element.parentNode.appendChild(temp);

            this.lineHeightCache[family][size] = temp.clientHeight;

            temp.parentNode.removeChild(temp);
        }

        return this.lineHeightCache[family][size];
    };

    this.timeoutCallback = function( event ) {
        // We need to check if we are still in polling mode.
        // If so, lets restart loading the current page.
        if ( keepServerCacheAlive.isReportFinished() ) {
            return;
        }

        $.Events.stopEvent( event );
        self.restartLoading();
    };

    // lets try iframe on an error
    // This relies on the postMessage function of newer browsers
    // and needs: if ( typeof parent != 'undefined' && parent.postMessage) {
    // parent.postMessage(document.body.scrollHeight, '*'); }
    // on the onload function of the loaded pages body.
    this.errorCallback = function () {

        if (dataHasJSONError(self.ajax.response)) {
            return;
        }

        // Build Frame
        self.scrollBeforeLoad.top = getId('__contentwrapper').scrollTop;
        self.scrollBeforeLoad.left = getId('__contentwrapper').scrollLeft;

        var menu = new MenubarActions();
        var iframe = document.createElement("iframe");
        iframe.id = menu.getCurrentPageName(self.pageNr, "__contentIFrame");
        iframe.name = iframe.id;

        iframe.setAttribute('scrolling', 'no');
        iframe.style.display = "none";

        var finished = false;
        var messageFunction = function (event) {

            finished = true;
            var data = event.data;
            // If this message does not come with what we want, discard it.
            if ((typeof data).toLowerCase() == "string" || !data.message) {
                new errorPacketHandler(self.responseEvaluationError);
                return;
            } else if (data.message != 'cssBody' + iframe.name) {
                // Not our message?
                return;
            }

            // Lets build the page!
            self.addStylesAndBodyToDom(data.css, data.body, data.permissions);
            iframe.parentNode.removeChild(iframe);

            // Clear the window Event after we are done!
            window.removeEvent("message", messageFunction);
        };

        // load event for the iframe to display the content
        iframe.addEvent('load', function () {

            // Check If we can send a postMessage
            if (iframe.contentWindow.postMessage) {

                // Register the Message Event for PostMessage receival
                window.addEvent("message", messageFunction);

                // Send a message
                var message = "getFrameContent" + iframe.name;
                iframe.contentWindow.postMessage(message, "*");
            } else {
                // if not, Replace and hide
                getId('__contentwrapper').style.display = "none";
                iframe.style.display = "block";
                self.finalizeLoadPage(iframe.id);
                finished = true;
            }

            getId('__contentwrapper').scrollTop = self.scrollBeforeLoad.top;
            getId('__contentwrapper').scrollLeft = self.scrollBeforeLoad.left;
        });

        window.setTimeout(function () {
            if (!finished) {
                self.pageNotFoundErrorHandler = new errorPacketHandler(self.pageNotfoundError);
            }
        }, TIMEOUTBEFOREERROR);

        iframe.src = self.ajax.requestFile + (typeof fragment != 'undefined' ? "#" + fragment : "");
        getId('__contentwrapper').parentNode.appendChild(iframe);
    };

    try {
        /* If this goes wrong, ignore it */
        this.ajax.onLocalFileFunction = this.errorCallback;
        this.ajax.xmlhttp.onerror = this.errorCallback;
        this.ajax.xmlhttp.ontimeout = this.timeoutCallback;
    } catch (e) {
        this.ajaxCanHandleErrors = false;
    }

    this.linkReplacer = function (matches) {

        var schema = matches[2];
        var urlpart = matches[3];
        var expression = "^((https?|file|ftp|smb|afp):(\/\/)?|(mailto|data):|" + escape(BASE).replace(new RegExp("\\+", "g"), "\\+") + ")";

        if (!urlpart.match(new RegExp(expression))) {
            // urlpart = buildBASEURLForPageFile(urlpart, false, {}, true); // 2018-08-07 Links should already have encoded parameters, but the prompts are not yet.
            // urlpart = buildBASEURLForPageFile(urlpart, false); // 2019-05-13  
            urlpart = buildBASEURLForPageFile(urlpart, false, {}, true); // 2022-10-07 keep previous parameters here, or e.g. reorder will be wrong  
        }

        if (urlpart.match(new RegExp("^#(.*?)$"))) {
            // ScrollToDiv
            urlpart += "\" onclick=\"if(!event){var event=window.event;}if(event){event.cancelBubble=true;event.returnValue=false;}if(event&&event.stopPropagation){event.stopPropagation();}if(event&&event.preventDefault){event.preventDefault();}getId('popupviewer_content').scrollTop=getId('" + ((urlpart == "#") ? "popupviewer_content" : urlpart
                .substr(1)) + "').offsetTop;return false;";
        }

        return matches[1] + schema + '="' + urlpart + '"' + matches[4];
    };

    this.inlineCssLinkReplacer = function (matches) {

        var urlpart = matches[2];
        var expression = "^((https?|file|ftp|smb|afp):(\/\/)?|(mailto|data):|" + escape(BASE).replace(new RegExp("\\+", "g"), "\\+") + ")";

        if (!urlpart.match(new RegExp(expression))) {
            urlpart = buildBASEURLForPageFile(urlpart, false); // 2019-05-13  
        }

        return matches[1] + urlpart + matches[3];
    };

    /**
     * Start loading of the current loadpage object
     */
    this.startLoading = function (cacheOnly) {

        // Set all parameters
        addParametersToAJAX(self.ajax, {
            page: pageNr + '.html'
        });

        addPromptToAjax && addPromptToAjax(self.ajax);

        // start background thread to keep the cache alive
        keepServerCacheAlive.startPolling(); // restart the polling mechanism

        var cachedPage = _pageCache.getPage(self.ajax);
        if (cachedPage) {
            debug("Loading Page #" + pageNr + " from Cache.");
            (new LoadingView()).show();
            this.setPageContent(cachedPage);
            return;
        }

        if (_pageCache.isPageLoading(self.ajax) || cacheOnly) {
            return;
        }

        // now we have all the stuff for the cache
        this.ajax.onLoading = function () {
            _pageCache.setPageLoading(self.ajax);
        };

        try {
            (new LoadingView()).show();
            debug("Newly Loading Page #" + pageNr + ".");
            self.ajax.runAJAX();
        } catch (e) {
            (new LoadingView()).hide();

            // If there was an error and an iframe is not yet being constructed:
            // throw up an error
            if (self.ajaxCanHandleErrors && getId((new MenubarActions()).getCurrentPageName()).nodeName != 'IFRAME') {
                debug("Error Loading Page #" + pageNr + ".");
                new errorPacketHandler(this.pageNotfoundError);
            } else {
                debug("Loading Page #" + pageNr + " with iFrame.");
                this.errorCallback();
            }
        }
    };

    /**
     * Stop loading of the current loadpage object
     */
    this.stopLoading = function () {
        if (this.loadingDone) {
            return;
        }

        // use the previously defined cancel method if still loading
        // it will also hide the loading view
        this.cancelLoading();
    };

    /**
     * restart loading of the current loadpage object.
     * Will also reset the cache
     */
    this.restartLoading = function () {
        // Reset the Cache on restart.
        _pageCache.setPage(self.ajax, null);

        // Show Hide pair so it does not flicker
        self.startLoading();
        (new LoadingView()).hide();
    };

    if (!dontStartJustNow) {
        self.startLoading();
    }
};

/*******************************************************************************
 * INITIALIZE
 ******************************************************************************/
var URI, BASE, PROMPT = ( window.htmlviewer ? window.htmlviewer.PROMPT : PROMPT ) || [],
    VARIABLES, KEYBOARD_LISTENER,

    /** Timeout of loading an internal frame before an error pops up. */
    TIMEOUTBEFOREERROR = 10000,

    /** This parameter needs to be reset after the first page request. */
    PROMPTONREFRESH,

    /** If the report has prompts ... */
    HASPROMPTS,

    /** DRILLDOWN and SUBREPORTS are only enabled in Online mode and not when the reports parameter is used */
    DRILLDOWNANDSUBREPORTSDISABLED = false,

    // Viewer Options
    HASNOGROUPTREE, HASNOGROUPTREE, HASNOZOOM, HASNOTEXTSEARCH, HASNOPRINTBUTTON, HASNOEXPORTBUTTON, HASNOPROMPTONREFRESH, CANSHOWPERMALINK, DEFAULTZOOM,
    
    GROUPTREEOPEN,

    /* Default Named Options for skaling the page: PAGE_FIT, PAGE_WIDTH, PAGE_HEIGHT */
    ZOOMINGOPTIONS = [];

/**
 * Initializes the htmlViewer.
 * @function
 */
var initHTMLViewer = function () {

    /**
     * Base URL for the content
     */
    URI = ((String)(URI || document.location)).parseUri();


    // Do not modify the base url
    //	if (URI.directory.substr(URI.directory.length - 1) == '/') {
    //		URI.directory = URI.directory.substr(0, URI.directory.length - 1);
    //	}

    PROMPT = ( window.htmlviewer ? window.htmlviewer.PROMPT : PROMPT ) || [],
    VARIABLES = Object.extend(URI.queryKey, {}); // 2019-05-13 Do not add the Prompts. They will always be added for the request.
    // Prompts using the index instead of the name will otherwise take over and a refresh has no effect.
    // See below. The list of prompts will be put into PROMPT and removed from VARIABLES. Otherwise a re-submit of the prompts will always have the prompts from VARIABLES

    // This parameter needs to be reset after the first page request.
    PROMPTONREFRESH = VARIABLES.promptonrefresh == 'true' || VARIABLES.promptonrefresh == '1';

    HASNOPROMPTONREFRESH = VARIABLES.haspromptonrefresh == 'false';
    HASPROMPTS = ( PROMPT.length > 0 && !HASNOPROMPTONREFRESH ) || PROMPTONREFRESH;
    // PROMPT.length = 0; // Reset Prompt // 2019-05-13 - do not reset. see above

    HASNOGROUPTREE = VARIABLES.hasgrouptree == 'false';
    GROUPTREEOPEN = VARIABLES.grouptreeopen == 'true';
    HASNOZOOM = VARIABLES.haszoomcontrol == 'false';
    HASNOTEXTSEARCH = VARIABLES.hastextsearchcontrols == 'false';
    HASNOPRINTBUTTON = VARIABLES.hasprintbutton == 'false';
    HASNOEXPORTBUTTON = VARIABLES.hasexportbutton == 'false';
    CANSHOWPERMALINK = typeof VARIABLES.canshowpermalink == 'undefined' || VARIABLES.canshowpermalink == 'true';

    // Prepare Zooming Options
    ZOOMINGOPTIONS = Object.freeze({
        PAGE_FIT: getTranslation("menuBar.zoom.pageFit"),
        PAGE_WIDTH: getTranslation("menuBar.zoom.pageWidth"),
        PAGE_HEIGHT: getTranslation("menuBar.zoom.pageHeight")
    });

    // Prepare Default Zoom
    DEFAULTZOOM = ensureDefaultZoom( (isNaN(VARIABLES.defaultzoom) ? ZOOMINGOPTIONS[VARIABLES.defaultzoom] : VARIABLES.defaultzoom) || $.jStorage.get("menu.zoom", getTranslation("menuBar.zoom.pageFit")) );

    delete(VARIABLES.canshowpermalink);
    delete(VARIABLES.hasgrouptree);
    delete(VARIABLES.grouptreeopen);
    delete(VARIABLES.haszoomcontrol);
    delete(VARIABLES.hastextsearchcontrols);
    delete(VARIABLES.defaultzoom);
    delete(VARIABLES.init);

    // If prompthandling was enabled - remove it now
    delete(VARIABLES.promptonrefresh);

    // This is the HTML Viewer - lets make that statement!
    VARIABLES.export_fmt = 'html';
    VARIABLES.viewer = 'html';

    if (VARIABLES.reports) {
        // Disable drilldown and subreports on demand. They do not work here.
        DRILLDOWNANDSUBREPORTSDISABLED = true;
    }

    if (VARIABLES.title) {
        document.title = VARIABLES.title;
    }

    // Set timezone offset if available
	VARIABLES.timezone = timezoneFromInternationalizationAPI();

    (function(){
        // Move away VARIABLEs starting with 'prompt'. 
        var VAR_KEYS = Object.keys( VARIABLES );
        var TMP_PROMPTS = [];
        for( var i=0; i<VAR_KEYS.length; i++ ) {
            var key = VAR_KEYS[i];
            if ( key.toLowerCase().indexOf( 'prompt' ) === 0 ) {
                // the key starts with 'prompt'
                TMP_PROMPTS.push({
                    name: key,
                    value: VARIABLES[key]
                });
                delete( VARIABLES[key] );
            }
        }

        if ( PROMPT.length === 0 ) {
            PROMPT = TMP_PROMPTS; // only set the new Prompts if they ware empty before.
        }
    })();

    // Globalize
    if ( window.htmlviewer ) {
        window.htmlviewer.PROMPT = PROMPT;
        window.HASPROMPTS = HASPROMPTS;
        window.HASNOPROMPTONREFRESH = HASNOPROMPTONREFRESH;

        window.htmlviewer.MenubarActions = MenubarActions;
        window.htmlviewer.popupHandler = popupHandler;
    }

    KEYBOARD_LISTENER = KEYBOARD_LISTENER || new keylistener();
    KEYBOARD_LISTENER.init();

    // Ok, the scripts are there, but the viewer.html is not.
    if (!getId('__contentwrapper') || !getId('__menuBarWrapper') || !getId('__grouptreewrapper')) {
        return; // don't bother.
    }
    // The base will need the file if one report or reports variable are set.
    // Otherweise it will screw up the URL.
    BASE = URI.protocol + '://' + URI.authority + URI.directory + URI.file;

    amIOnline.check(function (isOnline) {

        if (!isOnline) {
            // IF not online, this HAS TO BE the folder where the actual pages are in.
            BASE = BASE.substr(0, BASE.lastIndexOf('.'));
        }

        // Make sure the base does not end with a trailing slash. This will lead to problems
        // see #28765. The Slash will be added within the buildBASEURLForPageFile method.
        // 2015-02-16 as of now: The trailing / has to stay, e.g. the IIS needs it
        //        if ( BASE[BASE.length-1] == '/' ) {
        //            BASE = BASE.substr(0, BASE.length-1);
        //        }

        var menu = new menubar();
        (new tabbedPanel()).createTabBar();

        (window.htmlviewer || {}).GLOBAL_STARTUP_ERROR === true || menu.menubaractions.firstPage();
    });
};

$.Events.addInitEvent(initHTMLViewer);