Source: modules/menubar.js

/*******************************************************************************
 * MENU BAR ACTIONS
 ******************************************************************************/
/* global CANSHOWPERMALINK */
/* global DEFAULTZOOM */
/* global HASNOEXPORTBUTTON */
/* global HASNOPRINTBUTTON */
/* global HASNOTEXTSEARCH */
/* global HASNOZOOM */
/* global HASPROMPTS */
/* global IEVersion */
/* global KEYBOARD_LISTENER */
/* global PROMPTONREFRESH */
/* global VARIABLES */
/* global _pageCache */
/* global amIOnline */
/* global browserPrint */
/* global closeAllPopUps */
/* global debug */
/* global documentExport */
/* global documentSearch */
/* global editableComboBox */
/* global getId */
/* global getPageCount */
/* global getTranslation */
/* global grouptree */
/* global keepServerCacheAlive */
/* global loadpage */
/* global menubarLoading */
/* global pageCache */
/* global popupHandler */
/* global printListener */
/* global scrollToElement */
/* global tabbedPanel */
/* global vgen */
/* global waitForFinalEvent */
/* global windowSize */

var _menubaractions = null;

/**
 * The action on the menu bar. This is a singleton.
 * 
 * @class
 */
/* jshint -W098 */
var MenubarActions = function() {
/* jshint +W098 */

    // make singleton
    if (_menubaractions != null) {
        return _menubaractions;
    }
    _menubaractions = this; // Singleton Instance

    var self = this;
    this.currentZoom = 100;
    this.maxPages = null;
    this.enabledFormats = null;

    this.currentPageNumber = (function() {
        var currentPage = -1;
        return {
                        get : function() {
                            if (currentPage > (self.maxPages || currentPage)) {
                                debug("CurrentPage is out of scope (using maxPage): " + currentPage + " > " + self.maxPages);
                                return self.maxPages;
                            }

                            debug("CurrentPage is good: " + currentPage);
                            return currentPage;
                        },
                        set : function(page) {
                            currentPage = Math.max(0, Math.min(page, self.maxPages || page));
                            debug("SETTING PAGE NUMBER: " + page);
                            // debug("TRACE: " + (new Error()).stack);
                            return currentPage;
                        }
        };
    })();

    /** Zoom further in */
    this.zoomIn = function(e, slow) {
        self.zoom((slow ? 0.5 : 1) * (self.currentZoom >= 100 ? self.currentZoom / 4 : 5), e);
    };

    /** Zoom out */
    this.zoomOut = function(e, slow) {
        self.zoom((slow ? 0.5 : 1) * (self.currentZoom > 100 ? -self.currentZoom / 5 : -5), e);
    };

    // To be implemented, where needed
    this.updateZoomStatus = function(zoom) {
    };

    /**
     * Returns the page - element which is currently active, e.g. for loading
     * 
     * @param {number} contentWrapperInnerNumber Number of content wrapper to
     *            generate name for
     * @param {string} prefix Name prefix
     * @returns {string} name of the page for parameters
     */
    this.getCurrentPageName = function(contentWrapperInnerNumber, prefix) {

        var name = typeof prefix == 'undefined' ? '__content' : prefix;
        name += '-' + (new tabbedPanel()).frontMostTab();

        name += '-' + (getId('single-page').isCurrent() ? 1 : (contentWrapperInnerNumber != null ? contentWrapperInnerNumber : Math.max(1, self.currentPageNumber.get())));
        return name;
    };

    /**
     * Apply current zoom to inner Pages
     * 
     * @param {object} contentWrapperInner document element of the inner content
     *            wrapper
     * @param {object} content document element of the content
     */
    this.applyZoom = function(contentWrapperInner, content) {
        if (!content) {
            return;
        }
        var newZoom = (self.currentZoom / 100);

        content = content.parentNode;

        var newHeight = (content.offsetHeight || content.scrollHeight) * newZoom, newWidth = content.offsetWidth * newZoom;
        contentWrapperInner.style.height = newHeight > 0 ? newHeight + 'px' : '';
        contentWrapperInner.style.width = newWidth > 0 ? newWidth + 'px' : '';

        if (typeof content.style.transform !== 'undefined') {
            // In addition the defaults as well
            content.style.transform = "scale(" + newZoom + ")";
            content.style.transformOrigin = "50% 0%";
        } else if (typeof content.style.MozTransform !== 'undefined') {
            content.style.MozTransform = "scale(" + newZoom + ")";
            content.style.MozTransformOrigin = "50% 0%";
        } else if (typeof content.style.webkitTransform !== 'undefined') {
            content.style.webkitTransform = "scale(" + newZoom + ")";
            content.style.webkitTransformOrigin = "50% 0%";
        } else if (typeof content.style.OTransform !== 'undefined') {
            content.style.OTransform = "scale(" + newZoom + ")";
            content.style.OTransformOrigin = "50% 0%";
        } else if (typeof content.style.msTransform !== 'undefined') {
            content.style.msTransform = "scale(" + newZoom + ")";
            content.style.msTransformOrigin = "50% 0%";
        } else {
            content.style.zoom = newZoom * 100 + '%';
        }

        content.style.marginLeft = Math.round(-(1 - newZoom) / 2 * content.offsetWidth) + "px";
    };

    /**
     * Calculates the optimal width of the current tab so that the page is fully
     * visible
     * 
     * @returns {number} width in pixel
     */
    this.optimalWidth = function() {
        // Determine the width from the outer Wrapper. The wrapper will be
        // changed by the group tree if set.
        var contentWrapper = getId('__contentwrapper');
        if (contentWrapper.offsetWidth === 0) {
            contentWrapper = getId('__menuBarWrapper'); // For the time being. Usually the init phase
        }

        var outerWidth = contentWrapper.offsetWidth;
        if (!getId('__grouptreewrapper').isCurrent()) {
            // If hte group tree is not visible, use the parent node of the
            // contentwrapper
            // this is due to not knowing if this was already the case or we
            // just changed it
            outerWidth = contentWrapper.parentNode.offsetWidth;
        }

        if (contentWrapper.parentNode.offsetWidth === outerWidth) {
            // The group tree is maybe not visible yet. if the width differs, it
            // is probably visible
            // thus we need to include it
            outerWidth -= Number((contentWrapper.style.left || '0px').slice(0, -2));
        }

        var data = getPageCount.DATA || getPageCount.preliminaryDATA;
        var width = data && data.page ? data.page.width : outerWidth;
        if (window.getComputedStyle) {
            var style = window.getComputedStyle(getId(self.getCurrentPageName(null, '__contentwrapperinner')) || contentWrapper);
            outerWidth -= Number(style.paddingLeft.slice(0, -2)) + Number(style.paddingRight.slice(0, -2));
        } else {
            outerWidth -= 40;
        }

        return Math.floor(100 / width * outerWidth);
    };

    /**
     * Calculates the optimal height of the current tab so that the page is
     * fully visible
     * 
     * @returns {number} height in pixel
     */
    this.optimalHeight = function() {
        var outerHeight = getId('__contentwrapper').offsetHeight;

        var data = getPageCount.DATA || getPageCount.preliminaryDATA;
        var height = data && data.page ? data.page.height : outerHeight;
        if (window.getComputedStyle) {
            var style = window.getComputedStyle(getId(self.getCurrentPageName(null, '__contentwrapperinner')) || getId('__contentwrapper'));
            outerHeight -= Number(style.paddingTop.slice(0, -2)) + Number(style.paddingBottom.slice(0, -2));
        } else {
            outerHeight -= 40;
        }

        return Math.floor(100 / height * outerHeight);
    };

    /**
     * Set a specific zoom value, make sure the width/height is OK
     * 
     * @param {(string|number)} zoom number in percent, optionally with '%' or a
     *            specific string value
     * @param {Event} event An event that triggered the change. may be null. can
     *            be used to determine the center point
     */
    this.setSafeZoom = function(zoom, event) {
        if (zoom == getTranslation("menuBar.zoom.pageFit")) {
            var ratioPoint = 5;
            var ratio = getPageCount.getPageRatio();
            zoom = ratio <= 1 / ratioPoint ? getTranslation("menuBar.zoom.pageWidth") : (ratio > ratioPoint ? getTranslation("menuBar.zoom.pageHeight") : DEFAULTZOOM);
        }
        this.setZoom(zoom, event);
    };

    /**
     * Before the actual zoom is executed we need to prepare some data
     */
    var zoomBeforeAction = function( event ) {

        event = event || {};
        if ( event.type != "mousewheel" ) {
            // We only want scroll events here.
            event = {};
        }

        var zoomEventModel = {};

        // now check on the new ScrollPosition
        var wrapper = getId('__contentwrapper');
        var content = self.getCurrentTab();

        zoomEventModel.previousHeight = (content || wrapper).offsetHeight;
        zoomEventModel.previousZoom = self.currentZoom;
        zoomEventModel.scrollLeft = wrapper.scrollLeft;
        zoomEventModel.scrollTop = wrapper.scrollTop;

        zoomEventModel.mouseX = event.pageX ? (event.pageX || 0) - wrapper.offsetLeft : wrapper.offsetWidth / 2; // correction for group tree
        zoomEventModel.mouseY = (event.pageY || window.innerHeight / 2) - wrapper.offsetTop; // correction for menubar

        // debug( 'pos: ' + scrollLeft + ' ' + scrollTop );
        // debug( 'move: ' + mouseX + ' ' + mouseY );
        return zoomEventModel;
    };

    /**
     * After the actual zoom is executed we need to update some of the view
     */
    var zoomAfterAction = function( zoomModel ) {

        var wrapper = getId('__contentwrapper');
        var content = self.getCurrentTab();
        var currentHeight = (content || wrapper).offsetHeight;

        // CAlculate the new zoom
        var newZoom = zoomModel.previousZoom / zoomModel.previousHeight * currentHeight;

        // Move page in relation to zoom
        wrapper.scrollLeft = zoomModel.scrollLeft * newZoom / zoomModel.previousZoom;
        wrapper.scrollTop = zoomModel.scrollTop * newZoom / zoomModel.previousZoom;

        // debug( 'pos 1: ' + wrapper.scrollLeft + ' ' + wrapper.scrollTop );

        var moveX = Math.floor( zoomModel.mouseX - zoomModel.mouseX / zoomModel.previousZoom * newZoom );
        var moveY = Math.floor( zoomModel.mouseY - zoomModel.mouseY / zoomModel.previousZoom * newZoom );

        // debug( 'corr: ' + moveX + ' ' + moveY );

        // Correction for the cursor.
        wrapper.scrollLeft -= moveX;
        wrapper.scrollTop -= moveY;

        // debug( 'pos 2: ' + wrapper.scrollLeft + ' ' + wrapper.scrollTop );
    };

    /**
     * Set a specific zoom value
     * 
     * @param {(string|number)} zoom number in percent, optionally with '%' or a
     *            specific string value
     * @param {Event} event An event that triggered the change. may be null. can
     *            be used to determine the center point
     */
    this.setZoom = function(zoom, event) {

        if (HASNOZOOM) {
            return;
        }

        switch (zoom) {
        case getTranslation("menuBar.zoom.pageWidth"):
            zoom = self.optimalWidth() + '%';
            break;
        case getTranslation("menuBar.zoom.pageHeight"):
            zoom = self.optimalHeight() + '%';
            break;
        case getTranslation("menuBar.zoom.pageFit"):
            var width = self.optimalWidth();
            var height = self.optimalHeight();
            zoom = (width > height ? height : width) + '%';
            break;
        }

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

        var zoomEventModel = zoomBeforeAction( event );
        self.currentZoom = zoom;

        if (self.currentZoom <= 10) {
            self.currentZoom = 10;
        }
        if (self.currentZoom >= 10000) {
            self.currentZoom = 10000;
        }

        // Disable if too small already
        (getId('zoomOut') || document.createElement('div')).setEnabled(self.currentZoom > 10);

        // Disable if too large
        (getId('zoomIn') || document.createElement('div')).setEnabled(self.currentZoom < 10000);

        // Change the content elements for good
        var contentWrapper = getId('__contentwrapper');
        var contentWrapperInner = (self.getCurrentTab() || contentWrapper).getElementsByClassName('__contentwrapperinner');
        for (var i = 0; i < contentWrapperInner.length; i++) {
            self.applyZoom(contentWrapperInner[i], getId(self.getCurrentPageName(i + 1)));
        }

        zoomAfterAction( zoomEventModel );

        // Trigger Zoom which should reload a newly visible page
        if (getId('endless-mode').isCurrent()) {
            contentWrapper.fireEvent('scroll');
        }

        self.updateZoomStatus(self.currentZoom + '%');
    };

    /**
     * Do a zoom depending on current value and input.
     * 
     * @param {number} change New zoom
     * @param {Event} event An event that triggered the change. may be null.
     */
    this.zoom = function(change, event) {

        change = parseInt(change);
        var current = parseInt(self.currentZoom);
        /*
         * // von 100 als nullpunkt ausgehen change = change/5 + (current -
         * 100)^4; change = change^(1/4) + 100;
         */
        // Did something change? select the function!
        (change != 0 && !isNaN(change) ? self.updateZoom : self.setZoom)(Math.max(10, (change || 0) + current), event);

        // Fire Scroll Event for Page Loading in multipage - is done in
        // setZoom/applyZoom
        /*
         * if ( !getId('single-page').isCurrent() ) {
         * getId('__contentwrapper').fireEvent('scroll'); }
         */
    };

    /**
     * Save zoom to storage and update the UI
     * 
     * @param {(number|string)} zoom the zoom to set
     * @param {Event} event An event that triggered the change. may be null.
     */
    this.updateZoom = function(zoom, event) {

        // Set the new value for the zoom
        $.jStorage.set("menu.zoom", zoom);
        self.setZoom(zoom, event);
    };

    /**
     * To be implemented, where needed
     * 
     * @param {object} newPage the page
     */
    this.updatePageStatus = function(newPage) {
    };

    /**
     * Make sure that the addressed page is present in the DOM
     * 
     * @param {number} page number of page
     * @param {string} fragment URL fragement
     * @param {function} finalFunction function to call if page is loaded
     */
    this.assurePageIsPresent = function(page, fragment, finalFunction) {
        // Multipage Action - The Page HAS TO BE THERE
        if (!getId('single-page').isCurrent()) {

            // If already loaded, no problem
            page = self.getCurrentPageName(page);
            var pageElement = page ? getId(page) : null;

            if (pageElement && !pageElement.hasBeenLoaded) {
                // if not loaded, this has to fire.
                pageElement.onLoad = finalFunction;
                pageElement.scrollIntoView(true);
            } else if (pageElement && typeof finalFunction == 'function') {
                // If page is present but already loaded, just call.
                finalFunction();
            }

            return;
        }

        // Single Page Handling with loadPage Request
        self.setPage(page, finalFunction, fragment);
    };

    this.lastLoadPage = null;

    /**
     * Set the paging button status. Will disable if needed
     * 
     * @param {number} currentPage the page that is currently set
     */
    this.setPagingButtonStatus = function(checkPage) {

        var prevEnabled = checkPage > 1 || checkPage == -1;
        var nextEnabled = checkPage < parseInt(self.maxPages) || checkPage == -1;

        getId('previous').setEnabled(prevEnabled);
        getId('first').setEnabled(prevEnabled);

        getId('next').setEnabled(nextEnabled || self.maxPages === '');
        getId('last').setEnabled(nextEnabled);

        if (checkPage >= 0) {
            self.updatePageStatus(checkPage + "/" + (self.maxPages || ''));
        }
    };

    /**
     * Set the page and update loading status
     * 
     * @param {number} page number of page
     * @param {string} fragment URL fragement
     * @param {function} finalFunction function to call if page is loaded
     */
    this.setPage = function(page, finalFunction, fragment) {

        if (!page) {
            page = self.currentPageNumber.get() || 1;
        } else if (page != self.currentPageNumber.get()) {
            page = self.currentPageNumber.set(page);
        } else {
            // Nothing to do
            if (typeof finalFunction == 'function') {
                finalFunction(false);
            }

            // Extra update - may be differently
            self.setPagingButtonStatus(page);
            return;
        }

        if (this.lastLoadPage != null) {
            // stop? clean old loadPage;
            this.lastLoadPage.stopLoading();
        }

        if (getId('single-page').isCurrent()) {
            this.lastLoadPage = new loadpage(page, finalFunction, fragment);
        } else {
            var element = getId(this.getCurrentPageName(page, '__contentwrapperinner'));
            if (element) {
                scrollToElement(element);
                getId('__contentwrapper').fireEvent('scroll');
            }

            if (typeof finalFunction == 'function') {
                finalFunction(false);
            }
        }

        self.setPagingButtonStatus(page);
    };

    /**
     * Go to first Page
     * 
     * @param {function} finishFunction Function when the page has loaded
     * @param {string} fragment (optional) a fragment to jump to after being
     *            done.
     */
    this.firstPage = function(finishFunction, fragment) {
        self.setPage(1, finishFunction, fragment);
    };

    /**
     * Go to last Page
     * 
     * @param {function} finishFunction Function when the page has loaded
     * @param {string} fragment (optional) a fragment to jump to after being
     *            done.
     */
    this.lastPage = function(finishFunction, fragment) {
        self.setPage(self.maxPages, finishFunction, fragment);
    };

    /**
     * Go to next Page
     * 
     * @param {function} finishFunction Function when the page has loaded
     * @param {string} fragment (optional) a fragment to jump to after being
     *            done.
     */
    this.nextPage = function(finishFunction, fragment) {
        var page = parseInt(self.currentPageNumber.get()) + 1;
        if (self.maxPages && page > self.maxPages) {
            page = self.maxPages;
        }

        self.setPage(page, finishFunction, fragment);
    };

    /**
     * Go to previous Page
     * 
     * @param {function} finishFunction Function when the page has loaded
     * @param {string} fragment (optional) a fragment to jump to after being
     *            done.
     */
    this.previousPage = function(finishFunction, fragment) {
        var page = parseInt(self.currentPageNumber.get()) - 1;
        if (page < 1) {
            page = 1;
        }

        self.setPage(page, finishFunction, fragment);
    };

    /**
     * Print the report using our printListener if it is available or use the
     * window.print() function otherwise
     */
    this.printReport = function() {

        if (getPageCount.DATA === null) {
            return;
        }

        ( printListener && printListener.showPrint && printListener.showPrint() ) ||
        ( browserPrint && browserPrint.printInternal && browserPrint.printInternal() );
    };

    /**
     * Reload a report. reset prompts if needed
     */
    this.reloadReport = function() {
        /* jshint -W020 */
        vgen = null;
        VARIABLES.cmd = "rfsh";
        if (PROMPTONREFRESH) {
            VARIABLES.promptonrefresh = true;
        }

        _pageCache = new pageCache();
        /* jshint +W020 */

        // Start the loading indicator. Will be removed when the grouptree is
        // done.
        menubarLoading.start('reload');
        self.refreshReport(null);
    };

    /**
     * Should promptonrefresh be displayed or not this is determined by the
     * global HASPROMPTS variable
     */
    this.activatePromptOnRefresh = function() {

        var hasGlobalPrompts = (window.htmlviewer ? window.htmlviewer.HASPROMPTS : HASPROMPTS) || HASPROMPTS; // if the htmlviewer var exists but has not been used
        var reloadButtonRef = getId('reload');
        if (reloadButtonRef) {
            reloadButtonRef.addRemoveClass("hasprompts", hasGlobalPrompts);
        }

        var promptOnRefreshButtonRef = getId('promptonrefresh');
        if (promptOnRefreshButtonRef) {
            promptOnRefreshButtonRef.setIsCurrent(PROMPTONREFRESH);

            if (hasGlobalPrompts) {
                promptOnRefreshButtonRef.addRemoveClass('visible', true);
            } else {
                promptOnRefreshButtonRef.addRemoveClass('visible', false);
            }
        }
    };

    /**
     * Is it active? Determined by the global PROMPTONREFRESH variable
     */
    this.promptonrefresh = function() {
        getId('promptonrefresh').setIsCurrent(!PROMPTONREFRESH);
        window.PROMPTONREFRESH = !window.PROMPTONREFRESH;
    };

    /**
     * Resets the grouptree, closes all popups and loads the page again
     * 
     * @param {boolean} resetGroupTree full reset?
     * @param {function} finishFunction function when done loading the page
     */
    this.refreshReport = function(resetGroupTree, finishFunction) {
        (new grouptree(false)).reset(resetGroupTree);

        // Close Prompt Dialog
        closeAllPopUps && closeAllPopUps();

        this.activatePromptOnRefresh();

        // Next Action on reload depending on current Mode
        if (getId('single-page').isCurrent()) {
            self.singlePage(true, finishFunction);
        } else {
            self.endlessMode(true, finishFunction);
        }

        keepServerCacheAlive.startPolling(true); // restart the polling
        // mechanism
    };

    this.showKeyBindings = function( event ) {

        if ( window.htmlviewer && typeof window.htmlviewer.showShortCuts == 'function' ) {
            window.htmlviewer.showShortCuts( event );
            return;
        }

        var popup = new popupHandler().show();
        popup.addHeader(getTranslation("keyBinding.header"));

        var body = document.createElement('p');
        body.appendChild(document.createTextNode(getTranslation("keyBinding.body")));
        popup.addBody(body);

        KEYBOARD_LISTENER.registeredListener.forEach(function(entry) {
            popup.addDetail("[" + entry.keyCode + "]", entry.description, true);
        });
    };

    /**
     * Clean the content wrappers, full reset of the report pages
     */
    this.resetReportPages = function() {
        // Clear the events or they might get triggered instantly resulting in
        // massive delays on large reports.
        getId('__contentwrapper').clearEvents('scroll');
        // Clear current Tab - has to be reloaded anyways
        var contentWrapperInner = (self.getCurrentTab() || document).getElementsByClassName('__contentwrapperinner');
        var i = contentWrapperInner.length, elem = null;

        while (i > 0) {

            // Remove Content
            elem = getId(self.getCurrentPageName(i, '__contentwrapperinner'));
            if (elem) {
                elem.hasBeenLoaded = true;
                elem.parentElement.removeChild(elem);
            } else if (console.error) {
                // That should not happen!
                console.error("Element not found in reset: " + self.getCurrentPageName(i, '__contentwrapperinner'));
            }

            // Remove Style-Element if available
            var styleElement = getId(self.getCurrentPageName(i, '__pageStyles'));
            if (styleElement) {
                styleElement.parentNode.removeChild(styleElement);
            }

            i--;
        }
    };

    /**
     * Retursn the currenly active tab
     * 
     * @returns {object} the tab or null
     */
    this.getCurrentTab = function() {
        var tabPanel = new tabbedPanel();
        var tab = tabPanel.managedElements[tabPanel.frontMostTab()];
        return tab ? tab.tabWrapper : null;
    };

    /**
     * Enable single page mode
     * 
     * @param {boolean} override true to reset the report though single page
     *            mode is already the current mode
     * @param {function} finishFunction what to do when done loading the first
     *            page
     */
    this.singlePage = function(override, finishFunction) {
        if (getId('single-page').isCurrent() && !override) {
            return;
        }

        debug("Going into SINGLE Mode");
        self.resetReportPages();

        getId('single-page').setIsCurrent(true);
        getId('endless-mode').setIsCurrent(false);

        // Creat a new first page
        self.getCurrentTab().appendChild(self.createInnerContentWrapper());
        self.setPage(null, finishFunction);
    };

    /**
     * Enable endless page mode - this could be a huge performance hit for long
     * reports
     * 
     * @param {boolean} override true to reset the report though single page
     *            mode is already the current mode
     * @param {function} finishFunction what to do when done loading the first
     *            page
     */
    this.endlessMode = function(override, finishFunction) {
        if (getId('endless-mode').isCurrent() && !override) {
            return;
        }

        // Somehow cancel the current loading action
        debug("Going into ENDLESS Mode");
        self.resetReportPages();

        getId('single-page').setIsCurrent(false);
        getId('endless-mode').setIsCurrent(true);
        self.loadEndlessPage(self.currentPageNumber.get(), finishFunction);
    };

    /**
     * Function to prepare loading all pages and load them when they scroll into
     * view
     * 
     * @private
     */
    this.buildEndlessPageWithScrollListener = function(finishFunction) {

        var internalSelf = self;
        var containerElement = getId('__contentwrapper');
        var elementInViewport = function(el, mostlyVisible) {

            if (!el) {
                return false;
            }

            var elementTop = el.absoluteOffsetTop();
            var elementHeight = el.getBoundingClientRect ? el.getBoundingClientRect().height : el.offsetHeight;

            if (mostlyVisible) {
                // Check of the page is over the half.
                var center = windowSize().height / 2;

                if ( elementHeight < center ) {
                    center = elementHeight; // Sync to height of element to prevent jumping of pages
                }

                // Sync to the middle of the oage or elementHeight if too small
                return containerElement.scrollTop + center > elementTop && containerElement.scrollTop + center < elementTop + elementHeight;
            }

            var containerHeight = containerElement.getBoundingClientRect ? containerElement.getBoundingClientRect().height : containerElement.offsetHeight;
            return elementTop < (containerElement.scrollTop + containerHeight) && (elementTop + elementHeight) > containerElement.scrollTop;
        };

        var addScrollListener = function(pageNumber) {

            var timeoutHandler = null;
            var handlerFunction = function(me, isTimedCall) {

                var pageElement = getId(self.getCurrentPageName(pageNumber));
                var pageIsInViewPort = pageElement && elementInViewport(pageElement);
                if (pageIsInViewPort) {
                    var loadFunction = function() {
                        timeoutHandler = null;
                        debug("Loading (page is in viewport) for page #" + pageNumber);
                        containerElement.removeEvent('scroll', handler);

                        // Should have been canceld a long time ago
                        if (pageElement.hasBeenLoaded || getId('single-page').isCurrent()) {
                            return;
                        }

                        internalSelf.currentPageNumber.set(pageNumber);

                        new loadpage(pageNumber, function() {
                            if (pageElement.onLoad) {
                                pageElement.onLoad();
                            }
                            pageElement.hasBeenLoaded = true;
                            
                            // Start the cycle again and try to set the correct page number. again.
                            // containerElement.fireEvent('scroll');
                        });
                    };

                    if (isTimedCall) {
                        // override !
                        debug("Timed Call of loadFunction for Page #" + pageNumber);
                        timeoutHandler = null;
                        loadFunction();
                    } else if (timeoutHandler == null) {
                        // delay
                        debug("Calling handler again for page #" + pageNumber);
                        timeoutHandler = window.setTimeout(function(me) {
                            handlerFunction(me, true);
                        }, 200);
                        return;
                    } else {
                        debug("this page must have been loaded already #" + pageNumber);
                        pageElement.hasBeenLoaded = true;
                    }
                } else if (!pageElement) {
                    debug("canceling scroll event (page not in view and pageElement empty) before loading for page #" + pageNumber);
                    containerElement.removeEvent('scroll', handler);
                } else {
                    debug("page #" + pageNumber + " not visible.");
                }

                timeoutHandler = null;
            };

            // Try if it is in view while construction.
            debug("Adding ScrollHandler for page " + pageNumber);

            // wait for final event for pages. but set pagenumbers already
            var handler = waitForFinalEvent(handlerFunction, 500, "scrollListener#" + pageNumber);

            containerElement.addEvent('scroll', handler);
            containerElement.addEvent('scroll', function( event ) {
                if ( event.noMorePageUpdates ) {
                    return;
                }
                var pageElement = getId(self.getCurrentPageName(pageNumber));
                if (pageElement && elementInViewport(pageElement, true)) {

                    event.noMorePageUpdates = true; // we did find a page that is fully displayed. So we do not change the page number any more for this event.
                    if (pageNumber == self.currentPageNumber.get()) {
                        return;
                    }

                    self.currentPageNumber.set(pageNumber);
                    self.setPagingButtonStatus(pageNumber);
                }
            });

            handlerFunction();
        };

        let whenFinishedLoadingPageCount = function(finished) {

            // Wait with building the endless-page until the whole document is
            // ready.
            // This is needed for long running reports.
            if (!finished) {
                return setTimeout(function() {
                    getPageCount.supplyWithData(whenFinishedLoadingPageCount);
                }, 250);
            }

            var previousCurrentPage = Math.max(0, self.currentPageNumber.get()); // Needs
            // to
            // be
            // reset
            // later
            // or
            // nothing
            // works.
            var internalCurrentPage = 0;

            // Wait for the correct page count before adding the other pages and
            // scroll listener
            // Especially when reloading in multipage view we would get the
            // wrong page count otherwise
            debug("Preparing endless pages. Previous Page was: " + previousCurrentPage);
            self.setPagingButtonStatus(-1);

            let prepareEndlessPageContent = function() {
                if (internalCurrentPage < self.maxPages && getId('endless-mode').isCurrent()) {

                    internalCurrentPage++;
                    self.currentPageNumber.set(internalCurrentPage);
                    self.updatePageStatus("#" + internalCurrentPage);

                    // What is the current Page???
                    var nextPage = self.createInnerContentWrapper();
                    self.getCurrentTab().appendChild(nextPage);
                    addScrollListener(self.currentPageNumber.get());

                    // Try to fetch from cache
                    (new loadpage(internalCurrentPage, function() {
                        self.currentPageNumber.set(previousCurrentPage);
                    }, null, true)).startLoading(true);

                    setTimeout(prepareEndlessPageContent, 1); // Wait
                    // millisecond
                    // before
                    // proceeding.
                    // Should
                    // Deblock the
                    // GUI.
                } else {
                    self.currentPageNumber.set(previousCurrentPage);
                    self.setPagingButtonStatus(previousCurrentPage);
                    getPageCount.setSizeOfContent();
                    menubarLoading.stop('endless-mode');

                    if (typeof finishFunction == 'function') {
                        finishFunction();
                    }
                }
            };
            prepareEndlessPageContent();
        };
        whenFinishedLoadingPageCount(false);
    };

    /**
     * Endlessly load pages until all are found and in the DOM
     * 
     * @param {function} finishFunction function to execute when all have been
     *            loaded
     */
    this.loadEndlessPage = function(currentPageNumber, finishFunction) {
        menubarLoading.start('endless-mode');
        var pageNode = self.createInnerContentWrapper(1);
        self.getCurrentTab().appendChild(pageNode);

        setTimeout(function() {
            debug("Starting to load endless page #1");
            new loadpage(1, function() {
                self.buildEndlessPageWithScrollListener(function() {
                    self.currentPageNumber.set(1);
                    self.setPage(currentPageNumber, finishFunction);
                });
            });
        }, 1);
    };

    /**
     * Open a subreport denoted by the link
     * 
     * @param {object} link subreport link
     * @param {function} finishFunction Function to call when the subreport is
     *            open
     */
    this.openSubreport = function(link, finishFunction) {
        // Get report Variables;
        var subreportVariables = {
                        // subreport_ondemand is encoded here! do not again!
                        subreport_ondemand : (typeof link.queryKey.subreport_ondemand != 'undefined' ? link.queryKey.subreport_ondemand : null),
                        subreport : (typeof link.queryKey.subreport != 'undefined' ? link.queryKey.subreport : null),

                        /*
                         * 2018-06-21 remove the decode from tabname. It should
                         * never be encoded but did have problems when the node
                         * name had percent signs in it.
                         */
                        /*
                         * 2018-07-17 decode again, it has been encoded in
                         * grouptree.js
                         */
                        name : (typeof link.queryKey.tabname != 'undefined' ? decodeURIComponent(link.queryKey.tabname) : null)
        };

        var tabPanel = new tabbedPanel();
        tabPanel.createOnlineOnlyTab(subreportVariables, null, finishFunction);
    };

    /**
     * Create an inner Container that may contain the body
     * 
     * @param {number} contentWrapperInnerNumber the number of the inner content
     *            wrapper to create
     * @returns {object} the DOM element
     */
    this.createInnerContentWrapper = function(contentWrapperInnerNumber) {

        var id = self.getCurrentPageName(contentWrapperInnerNumber, '__contentwrapperinner');
        var idElem = getId(id);
        if (idElem) {
            idElem.parentNode.removeChild(idElem);
            return idElem;
        }

        var contentWrapper = document.createElement('div');
        contentWrapper.addClassName('__contentwrapperinner');
        contentWrapper.id = id;

        var pagewrapper = document.createElement('div');
        pagewrapper.addClassName('__pagewrapper');
        contentWrapper.appendChild(pagewrapper);

        var content = document.createElement('div');
        content.addClassName('__content');
        content.id = self.getCurrentPageName(contentWrapperInnerNumber);
        pagewrapper.appendChild(content);

        return contentWrapper;
    };

    /**
     * Update the permissions-based buttons
     * 
     * @param {object} the page permissions
     */
    this.updatePermissions = function(permissions) {

        var menu = new menubar(), printMenu = (printListener.printMenu || {}).submenu;
        if (permissions && typeof documentExport == 'object' && !HASNOEXPORTBUTTON) {
            documentExport.enabledFormats = permissions.enabledFormats;
            window.CANSHOWPERMALINK = permissions.canShowPermalink != null ? permissions.canShowPermalink : window.CANSHOWPERMALINK;

            amIOnline.check(function(isOnline) {
                if (isOnline && !documentExport.exportMenu && printMenu && documentExport.exportActions().length > 0) {
                    documentExport.buildExport(menu.createButton("S", "save", getTranslation("menuBar.save"), documentExport.showExport, printMenu, null, null), printMenu);
                }
            });
        }

        // Check permissions.
        if ((permissions && !permissions.allowprint) || HASNOPRINTBUTTON) {
            // Printing enabled.
            if (printMenu && printMenu.id == 'print') {

                var nextItem = printMenu.previousSibling;
                printMenu.parentNode.removeChild(printMenu);
                printMenu = nextItem;
            }
        }

        var numberOfPagesKnown = getPageCount.DATA != null;

        // Disable Printing until the report is finished
        if (printMenu && printMenu.id == 'print') {
            printMenu.setEnabled(numberOfPagesKnown);
        }

        // Enable the save menu
        (getId('save') || document.createElement('div')).setEnabled(numberOfPagesKnown);

        // Enable the search menu
        (getId('search') || document.createElement('div')).setEnabled(numberOfPagesKnown);
    };
};

/*******************************************************************************
 * MENU BAR
 ******************************************************************************/
var _menubar = null;    

/**
 * The MenuBar - this is a singleton
 * 
 * @class
 */
var menubar = function() {

    if (_menubar != null) {
        return _menubar;
    }

    var self = this;
    _menubar = this;
    this.menubaractions = new MenubarActions();
    this.moreTabsSubmenu = null;
    this.endlessModeRef = null;

    /**
     * Create a button for the menubar
     * 
     * @param {(object|string)} accesskey the access key code or object with {
     *            keycode: {string}, shift: {boolean} }
     * @param {string} imageClass CSS class for the image to use and also the ID
     *            (not twice!)
     * @param {string} name Title of the button
     * @param {function} action function to execute when pressed or touch ended
     * @param {object} insertBefore DOM Object to insert the new button before
     * @param {string} hint what the button is about
     * @param {object} [insertInto='#__menuBar'] Where to add the button.
     * @param {string} [iconClass='__icon'] CSS Class to add for an icon to be
     *            shown
     * @returns {object} the Button
     */
    this.createButton = function(accesskey, imageClass, name, action, insertBefore, hint, insertInto, iconClass) {

        var button = document.createElement("div");
        button.id = imageClass;
        button.className = "__menuButton";

        if (!insertInto) {
            insertInto = getId('__menuBar');
        }

        if (typeof iconClass == 'undefined') {
            iconClass = '__icon';
        } else if ( !!name ) {
            var nameSpan = document.createElement("span");
            nameSpan.appendChild(document.createTextNode(name));
            button.appendChild(nameSpan);
        }

        if (imageClass) {
            button.addClassName(iconClass);
            button.addClassName(imageClass.split('.').pop());
            button.append(document.createElement('i'));
        }

        if (typeof insertBefore != 'undefined' && insertBefore != null) {
            insertInto.insertBefore(button, insertBefore);
        } else {
            insertInto.appendChild(button);
        }

        if (hint && hint.length > 0) {
            button.setAttribute('data-hint', hint);
        } else {
            button.title = name;
        }

        var executeAction = function(e) {
            KEYBOARD_LISTENER.stopEvent( e );
            button.isEnabled() && action.call( button, e );
        };

        button.addEvent('click', executeAction);
        button.addEvent('touchend', function(e) {
            e.preventDefault();
            executeAction.call(this, e);
        });

        if (typeof accesskey == 'string') {
            accesskey = {
                keycode : accesskey,
                shift : false
            };
        }

        if (accesskey && accesskey.keycode.length == 1 && accesskey.keycode >= 'A' && accesskey.keycode <= 'z') {

            var keyCode = "CTRL+";
            keyCode += accesskey.shift ? "SHIFT+" : '';
            keyCode += accesskey.keycode;

            if (hint && hint.length > 0) {
                button.setAttribute('data-hint', hint + " [" + keyCode + "]");
            } else {
                button.title += " [" + keyCode + "]";
            }

            button.setAttribute("tabindex", -1);

            var keydownEvent = function(event) {

                let keycode;
                if (event.which) {
                    keycode = event.which;
                } else {
                    keycode = event.keyCode;
                }

                if ((event.metaKey || event.ctrlKey) && ((!event.shiftKey && !accesskey.shift) || (event.shiftKey && accesskey.shift)) && String.fromCharCode(keycode).toLowerCase() == accesskey.keycode.toLowerCase()) {
                    KEYBOARD_LISTENER.stopEvent( event );
                    button.focus();
                    executeAction(event);
                }

            };

            KEYBOARD_LISTENER.addKeyListener(keyCode, name);
            window.addEvent('keydown', keydownEvent);

            button.clearButton = function() {
                this.clearEvents('click');
                this.clearEvents('touchend');
                window.removeEvent('keydown', keydownEvent);
                this.parentNode.removeChild(this);
            };
        }

        return button;
    };

    /**
     * Create a group where, e.g. buttons can be added
     * 
     * @param {object} [insertBefore=null] DOM element to insert the group
     *            before - at the end if null
     * @returns {object} DOM element of the group
     */
    this.createGroup = function(insertBefore) {
        var group = document.createElement("div");
        group.className = '__menuGroup';
        if (typeof insertBefore != 'undefined' && insertBefore != null) {
            getId('__menuBar').insertBefore(group, insertBefore);
        } else {
            getId('__menuBar').appendChild(group);
        }

        return group;
    };

    /**
     * Create a spacer with the size of a button
     * 
     * @param {object} [insertBefore=null] DOM element to insert the group
     *            before - at the end if null
     * @returns {object} DOM element of the group
     */
    this.createSpacer = function(insertBefore) {
        var spacer = document.createElement("div");
        spacer.className = "__menuSpacer";

        if (typeof insertBefore != 'undefined') {
            getId('__menuBar').insertBefore(spacer, insertBefore);
        } else {
            getId('__menuBar').appendChild(spacer);
        }

        return spacer;
    };

    /**
     * Create a dropdown box
     * 
     * @param {string} name Name and ID of the box
     * @param {Array} values List of values
     * @param {function} action The action to call for a selected value
     * @param {string} defaultValue The default value from the list
     * @param {object} insertInto Which button to replace with the dropdown
     * @param {string} className Extra class names for the dropdown
     * @returns {object} The dropdown element
     */
    this.createDropDown = function(name, values, action, defaultValue, insertInto, className) {

        var dropdown = new editableComboBox(name, className || '');
        dropdown.combobox.id = name;
        dropdown.combobox.addClassName(className || '');
        dropdown.combobox.addClassName("__menuDropDown");
        dropdown.updateAction = action;

        // Not an object / array? make one!
        if (typeof values != 'object') {
            values = [ values ];
        }

        // Add all options
        for ( var i = 0; i < values.length; i++) {
            dropdown.addOption(values[i], !isNaN(values[i]) ? '%' : '');
        }

        if (!insertInto) {
            insertInto = getId('__menuBar');
        }

        insertInto.appendChild(dropdown.combobox);
        if (typeof dropdown.updateAction == 'function') {
            dropdown.updateAction(defaultValue);
        }
        return dropdown;
    };

    /**
     * Set up the initial state of the menubar and create the buttons
     */
    this.init = function() {
        // Create Buttonbar
        // this.createSpacer().addClassName('hidden-mobile-xs');

        var singlePageRef = this.createButton("O", "single-page", getTranslation("menuBar.singlePage"), function() {
            self.menubaractions.singlePage();
        });
        singlePageRef.setIsCurrent(true);

        self.endlessModeRef = this.createButton("M", "endless-mode", getTranslation("menuBar.endlessPage"), function() {
            self.menubaractions.endlessMode();
        });
        self.endlessModeRef.setEnabled(false);
        // self.endlessModeRef.addClassName('hidden-mobile-xs');

        this.createSpacer();

        var pageGroup = this.createGroup();

        // Page Skip buttons
        this.createButton("<<", "first", getTranslation("menuBar.first"), self.menubaractions.firstPage, null, null, pageGroup);
        this.createButton("<", "previous", getTranslation("menuBar.previous"), self.menubaractions.previousPage, null, null, pageGroup);

        var pageDropper = this.createDropDown("page", [], null, 100, pageGroup);
        self.menubaractions.updatePageStatus = pageDropper.updateInputValue;
        // Umleiten der Action

        // If this is an online Version, print the buttons.
        amIOnline.check(function(isOnline) {
    
            // 2021-02-05 - The Edge browser has an issue with printing. That is why we do not register the printing Key "P" anymore
            var printKey = (IEVersion().Version.toLowerCase() == 'na' || IEVersion().Version < 20 ) ? "P" : null;

            if (!isOnline) {
                let printButton = self.createButton(printKey, "print", getTranslation("menuBar.print"), self.menubaractions.printReport, singlePageRef, null, null);
                printButton.addClassName('hidden-mobile-xs');
                printListener.buildPrint(printButton, singlePageRef, false);
                return;
            }

//*
            self.createButton("R", "reload", getTranslation("menuBar.reload"), self.menubaractions.reloadReport, singlePageRef);
            self.createButton({
                            keycode : 'R',
                            shift : true
            }, "promptonrefresh", getTranslation("menuBar.promptonrefresh"), self.menubaractions.promptonrefresh, singlePageRef, getTranslation("menuBar.promptonrefresh.hint"));
//*/
            self.menubaractions.activatePromptOnRefresh();

            if (typeof documentSearch == 'object' && !HASNOTEXTSEARCH) {
                var searchButton = self.createButton("F", "search", getTranslation("menuBar.search"), documentSearch.showSearch, singlePageRef, null, null);
                searchButton.addClassName('hidden-mobile-xs');
                documentSearch.buildSearch(searchButton, singlePageRef);
            }

            self.createSpacer(singlePageRef).addClassName('hidden-mobile-xs');

            // self.createSpacer();
            // Insert this before the reloadbutton.
            let printButton = self.createButton(printKey, "print", getTranslation("menuBar.print"), self.menubaractions.printReport, singlePageRef, null);
            printButton.addClassName('hidden-mobile-xs');
            printListener.buildPrint(printButton, singlePageRef, true);

            self.createSpacer(singlePageRef);
            self.menubaractions.updatePermissions();
        });

        pageDropper.input.parentNode.addClassName('hidden-mobile-xs'); // Page
        // selection
        // only
        // from
        // sm
        // upwards
        pageDropper.input.style.textAlign = 'center';
        pageDropper.input.style.width = '65px';

        pageDropper.updateAction = function(selectedValue) {
            // Match for number or number/number - otherwise nothing is done
            var match = selectedValue.match(new RegExp("^([0-9]+|([0-9]+)\/[0-9]+)$", "i"));
            var newPage = match ? Math.min(Math.max(1, match[2] ? match[2] : match[1]), self.menubaractions.maxPages) : 1;
            if (newPage) {
                self.menubaractions.setPage(newPage);
            }
        };

        this.createButton(">", "next", getTranslation("menuBar.next"), self.menubaractions.nextPage, null, null, pageGroup);
        this.createButton(">>", "last", getTranslation("menuBar.last"), self.menubaractions.lastPage, null, null, pageGroup);

        this.createSpacer();

        // Zoom buttons
        if (!HASNOZOOM) {

            var zoomGroup = this.createGroup();

            this.createButton("-", "zoomOut", getTranslation("menuBar.zoomOut"), self.menubaractions.zoomOut, null, null, zoomGroup);

            // DEFAULTZOOM because it will be stored again.
            var zoomStatus = this.createDropDown("zoom", [ 10, 25, 50, 75, 100, 150, 200, 300, 400, getTranslation("menuBar.zoom.pageWidth"), getTranslation("menuBar.zoom.pageHeight"), getTranslation("menuBar.zoom.pageFit") ], self.menubaractions.updateZoom, DEFAULTZOOM, zoomGroup);

            zoomStatus.input.style.width = '114px';
            zoomStatus.combobox.addClassName('hidden-mobile');

            self.menubaractions.updateZoomStatus = zoomStatus.updateInputValue; // Umleiten der Action
            this.createButton("+", "zoomIn", getTranslation("menuBar.zoomIn"), self.menubaractions.zoomIn, null, null, zoomGroup);
        }

        this.createSpacer();
        this.createButton("?", "help", getTranslation("menuBar.help"), self.menubaractions.showKeyBindings, null, null);
    };

    // Init if there is the menubar wrapper
    if (getId('__menuBarWrapper')) {
        this.init();
    }
};