Source: modules/grouptree.js

/*******************************************************************************
 * GROUP TREE
 ******************************************************************************/
/* global GROUPTREEOPEN */
/* global HASNOGROUPTREE */
/* global KEYBOARD_LISTENER */
/* global MenubarActions */
/* global VARIABLES */
/* global getId */
/* global getPageCount */
/* global getTranslation */
/* global highlightElements */
/* global menubar */
/* global menubarLoading */
/* global scrollToElement */

var _grouptree = null;
/**
 * GROUP TREE
 * @class
 * @returns {object} The group tree
 */
/* jshint -W098 */
var grouptree = function (init) {
/* jshint +W098 */

    // Make singleton
    if (_grouptree != null) {
        return _grouptree;
    }

    var self = this;
    this.grouptree = getId('__grouptree');
    this.grouptreeWrapper = getId('__grouptreewrapper');
    this.shouldInit = init || false;

    this.longTouchTimeout = null;
    this.lastClickedEntry = null;

    /**
     * Event Wrapper for each group tree node entry
     * @param   {object}   link the link that is going to be opened 
     * @param   {string}   type the type of event that we want
     * @returns {function} the function that will be used for the type
     */
    this.openGroup = function (link, type) {
        var groupSelf = this;
        this.link = link;
        
        /**
         * The click handler which is debounced and will handle jumping to a page and highlighting a section
         * @param {object} event the event
         */
        this.onClick = function (event) {

            // Only execute if the attribute is present.
            if ( !groupSelf.link.itemData || !groupSelf.link.itemData.hasAttribute('data-dblclick', 1) ) { return; }
            groupSelf.link.itemData.removeAttribute("data-dblclick");

            self.highlightSelectedElement(groupSelf.link);
            (new MenubarActions()).assurePageIsPresent(groupSelf.link.link.page, groupSelf.link.link.section, function () {
                self.jumpToSelectedGroupElement(groupSelf.link);
            });
        };
        
        /**
         * Wrapper to debounce the double click event
         * @param {object} event the event
         */
        this.checkClick = function( event ) {
            if ( groupSelf.link.itemData.getAttribute('data-dblclick') == null ) {
                groupSelf.link.itemData.setAttribute('data-dblclick', 1);
                setTimeout(function(){ groupSelf.onClick(event); }, 300);
            } // otherwise dblclick will fire
        };

        /**
         * Execute double click action, that is: open the subreport
         * @param {object} event the event
         */
        this.onDBLClick = function ( event ) {

            groupSelf.link.itemData.removeAttribute("data-dblclick");
            if (!groupSelf.link.url) {
                // ignore ... there is no url set.
                return;
            }

            var link = {};
            link.queryKey = {};

            // If this is a subreport, we need this data as well!!!
            link.queryKey.subreport_ondemand = VARIABLES.subreport_ondemand;
            
            /* 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 Encode here so it can safely be decoded in menubar.js */
            link.queryKey.tabname = encodeURIComponent( groupSelf.link.node );
            // link.queryKey.subreport = '&type=3&' + decodeURIComponent(groupSelf.link.url);
            // Needs to be a parameter now - so we have to encode ourself
            link.queryKey.subreport = encodeURIComponent('&type=3&' + groupSelf.link.url);

            groupSelf.link = groupSelf.link.link;
            (new MenubarActions()).openSubreport(link, function () {
                self.highlightSelectedElement(groupSelf);
                self.jumpToSelectedGroupElement(groupSelf);
            });
        };

        /**
         * Touch Handler. Detects a single or a double tap touch
         * @param {object} event the event
         */
        this.onTouchStart = function ( event ) {
            if (!event.touches || event.touches.length != 1) {
                return;
            }

            if ( groupSelf.link.itemData.getAttribute('data-dblclick') == null ) {
                groupSelf.link.itemData.setAttribute('data-dblclick', 1);
                setTimeout(function(){ groupSelf.onClick(event); }, 300);
            } else {
                groupSelf.onDBLClick( event );
            }
        };

        switch (type) {
            case 'click':
                return this.checkClick;
            case 'dblclick':
                return this.onDBLClick;
            case 'touchstart':
                return this.onTouchStart;
        }

        return null;
    };
    
    /**
     * Highlight a given tree element
     * @param {object} element the tree element to highlight
     */
    this.highlightSelectedElement = function( element ) {
        if (self.lastClickedEntry && self.lastClickedEntry.itemData) {
            self.lastClickedEntry.itemData.removeClassName('active');
        }
        if (element && element.itemData) {
            element.itemData.addClassName('active');
        }
        self.lastClickedEntry = element;
    };

    /**
     * Jump to the given tree element
     * @param {object} groupSelf the tree element
     */
    this.jumpToSelectedGroupElement = function (groupSelf) {

        var elems = document.getElementsByName(groupSelf.link.section);

        if (!elems || elems.length === 0) {
            return;
        }

        var i, listOfFading = [];
        for(i = 0; i < elems.length; i++) {
            var elem = elems[i];

            if (i === 0) {
                // Nur beim ersten Element springen, sonst nicht.
                scrollToElement(elem);
            }

            listOfFading.push(elem);
        }

        highlightElements(listOfFading);
    };

    /**
     * Open or close the the current node
     */
    this.openCloseNode = function( event ) {
        var element = this.parentNode.parentNode;
        element.addRemoveClass('open', !element.hasClassName('open'));
        return KEYBOARD_LISTENER.stopEvent( event );
    };
    
    /**
     * Creates the subtree starting with the currentElement and depth
     * @param   {object}  currentElement the element to start with
     * @param   {number}  currentDepth   the current menu depth
     * @returns {object} the subtree structure
     */
    this.buildTree = function (currentElement, currentDepth, parentElement, openTreeStructure) {

        if (!checkPageCountData(currentElement)) {
            return false;
        }

        var currentGroup = document.createElement("ul");
        currentGroup.addClassName('__group');

        var prevSibling = null;
        for (var i in currentElement) {
            var element = currentElement[i];
            if (!element.node && element.node !== "") {
                continue;
            }

            // Container
            var currentItem = document.createElement("li");
            currentItem.className = '__groupItem';

            // Actual Node
            var currentNode = document.createElement("span");
            currentNode.className = '__groupNode';
            currentNode.style.textIndent = (currentDepth +1 ) * 15 + 'px';
            currentNode.appendChild(document.createTextNode(element.node.length > 0 ? element.node : String.fromCharCode(160)));
            currentItem.appendChild(currentNode);

            element.itemData = currentNode; // Backreference
            currentNode.addEvent('click', new self.openGroup(element, 'click'));
            currentNode.addEvent('dblclick', new self.openGroup(element, 'dblclick'));
            currentNode.addEvent('touchstart', new self.openGroup(element, 'touchstart'));

            // back up
            element.parent = parentElement;
            element.prevSibling = prevSibling;
            element.nextSibling = null;
            if ( prevSibling != null ) {
                prevSibling.nextSibling = element;
            }
            prevSibling = element;

            var isOpenNodeStructure = openTreeStructure ? openTreeStructure[element.url] : undefined;

            var children = self.buildTree(element.childs, currentDepth + 1, element, isOpenNodeStructure);
            if (children !== false ) {

                if ( GROUPTREEOPEN || isOpenNodeStructure !== undefined ) {
                    currentItem.addClassName('open');
                }

                // add open close handler
                var openCloseToggle = document.createElement("div");
                openCloseToggle.addClassName('openCloseToggle icon next');
                openCloseToggle.style.left = (currentDepth +1 ) * 15 + 'px';
                currentNode.appendChild(openCloseToggle);
                openCloseToggle.addEvent('click', this.openCloseNode);

                currentItem.appendChild(children);
            }

            currentGroup.appendChild(currentItem);
        }

        return currentGroup;
    };

    /**
     * Walk the group tree using keyboard down
     * @private
     * @param   {object} element the element from the group tree
     * @returns {object} element that will be jumped to
     */
    this.__findNextJumpNode = function ( element ) {
        
        if ( element.itemData.parentNode.hasClassName('open') ) {
            // open, first. If that is null, god help us!
            return element.childs[0];
        }
        
        if ( element.nextSibling !== null ) {
            // all clear, node exists
            return element.nextSibling;
        }

        // Check nodes back up.
        while ( element.parent != null ) {
            element = element.parent;
            if ( element.nextSibling != null ) {
                return element.nextSibling;
            }
        }

        // Dead end
        return null;
    };

    /**
     * Walk the group tree using keyboard up
     * @private
     * @param   {object} element the element from the group tree
     * @returns {object} element that will be jumped to
     */
    this.__findPrevJumpNode = function ( element ) {
        
        if ( element.prevSibling == null ) {
            // First in line
            return element.parent;
        }
        
        element = element.prevSibling;
        
        while( element.itemData.parentNode.hasClassName('open') ) {
            // walk the node to its end of open items
            element = element.childs[element.childs.length -1];
        }
        
        return element;
    };

    /**
     * This is for keyboard input only and jumps
     * into the direction on the tree
     * @param {number} keycode that was entered
     */
    this.keyboardNavigation = function( keycode ) {

        // No group tree navigation if the tree is invisible
        if ( !self.grouptreeWrapper.isCurrent() ) { return; }
        
        var newNode = null;
        if ( !self.lastClickedEntry ) {
            // first time in here
            var data = getPageCount.DATA.grouptreeData;
            if (!checkPageCountData(data)) {
                // Nothing to do!
                return;
            }

            newNode = data[0];
        } else {
            // Node not empty
            switch( keycode ) {
                case 37: // left
                    if ( self.lastClickedEntry.itemData.parentNode.hasClassName('open') ) {
                        self.lastClickedEntry.itemData.parentNode.removeClassName('open');
                        return;
                    }
                    // Fall through and select previous.
                    /* fall through */
                case 38: // up
                    newNode = self.__findPrevJumpNode( self.lastClickedEntry );
                    break;
                case 39: // right
                    if ( !self.lastClickedEntry.itemData.parentNode.hasClassName('open') && self.lastClickedEntry.childs.length > 0 ) {
                        self.lastClickedEntry.itemData.parentNode.addClassName('open');
                        return;
                    }
                    // Fall through and select next.
                    /* fall through */
                case 40: // down
                    newNode = self.__findNextJumpNode( self.lastClickedEntry );
                    break;
            }
        }
            
        if (newNode == null) {
            return;
        }

        newNode.itemData.fireEvent('click');
    };

    /**
     * Reset the grouptree so that it will be loaded
     * again when requesting a new page
     * 
     * @param {boolean} [full=true] really load or just remove the tree from DOM
     */
    this.reset = function (full) {

        if (full) {
            self.showTree(false);
        }

        // Reset all the content when reloading, so the Groutree will be loaded
        if (full || full == null || typeof full === undefined) {
            // war disabled ... warum?
            _grouptree = null;
            getPageCount.reset();

            // A reload will reset the whole group tree. But the event will persist.handleErrorPage
			var handle = getId('__grouptreehandle'), divider = getId('__grouptreedivider');             
			handle && handle.clearEvents('click');
            divider && divider.clearEvents('mousedown') &&
            divider.clearEvents('touchstart') &&
            divider.clearEvents('dblclick');
        }

        var group = getId("__grouptree").firstChild;
        while (group) {
            getId("__grouptree").removeChild(group);
            group = getId("__grouptree").firstChild;
        }

        var grouptreehandle = getId('__grouptreehandle');
        if ( grouptreehandle && grouptreehandle.clearButton ) {
            grouptreehandle.clearButton();
        }
        
        return self;
    };

    /**
     * Show/hide the grouptree
     * @param {boolean} visible show the group tree
     */
    this.showTree = function (visible) {
        
        var contentWrapper = getId('__contentwrapper');
        var grouptreehandle = getId('__grouptreehandle');
        
        if (!visible) {
            self.grouptreeWrapper.setIsCurrent(false);
            grouptreehandle.addClassName('next');
            contentWrapper.style.left = 0;
        } else {
            self.grouptreeWrapper.setIsCurrent(true);
            contentWrapper.style.left = self.grouptreeWrapper.offsetWidth + 'px';
            grouptreehandle.removeClassName('next');
        }

        // Update zoom, eg. when in fit size mode
        (new MenubarActions()).setSafeZoom($.jStorage.get("menu.zoom"), {}); // Update zoom with scrolling
    };

    // Internal function to check if the pagecount data is usable for the group tree
    var checkPageCountData = function (currentElement) {
        return !(!currentElement || typeof currentElement != 'object' || Object.keys(currentElement).length === 0);
    };

    /**
     * Initializes a newly created instance of the grouptree
     * @returns {grouptree} the grouptree object
     */
    this.init = function() {

        // Already done?!
        if (self.grouptree.children.length > 0) {
            return;
        }

        // Globally disabled!
        if ( HASNOGROUPTREE ) {
            return;
        }

        self.grouptreeWrapper.style.width = $.jStorage.get('groupTreeWidth', 200) + 'px';

        // set up the click handler
        (new menubar()).createButton("J", '__grouptreehandle', null, self.toggleTree, null, getTranslation('groupTree.handle.hint'), getId('__grouptreewrapper'), '__icon previous right');

        var divider = getId('__grouptreedivider');
        divider.addEvent('mousedown', this.dragStart);
        divider.addEvent('touchstart', this.dragTouchStart);
        divider.addEvent('dblclick', this.sizeToFit);
        
        getPageCount.supplyWithData(function (data) {
            var addTree = false;
            if (self.grouptree.children.length === 0) {
                addTree = self.buildTree(data.grouptreeData, 0, null, data.groupTreeOpenStructure);
            }

            // Might be enabled
            menubarLoading.stop('reload');

            if (addTree !== false ) {
                self.grouptree.appendChild(addTree);
                self.grouptreeWrapper.style.display = 'block';

                // Show tree if it was not hidden before.
                self.showTree($.jStorage.get('groupTreeVisible', true));
            } else {
                // Do not show tree
                self.grouptreeWrapper.style.display = 'none';
                self.showTree(false);
            }
        });

        return self;
    };

    /**
     * Toggle open / close of the group tree
     * Has to be called as event handler
     */
    this.toggleTree = function() {
        self.showTree( !self.grouptreeWrapper.isCurrent() );
        $.jStorage.set('groupTreeVisible', self.grouptreeWrapper.isCurrent());
    };
    
    /**
     * DragStart of the grouptree divider to add document event listener
     * @param   {object}  event the event object
     * @returns {boolean} this is always fals to prevent further actions
     */
    this.dragStart = function( event ) {
        document.documentElement.addEvent('mousemove', self.drag);
        document.documentElement.addEvent('mouseup', self.dragStop);
        document.documentElement.addEvent('touchmove', self.drag);
        document.documentElement.addEvent('touchend', self.dragStop);
        return KEYBOARD_LISTENER.stopEvent( event );
    };
    
    this.dragTouchStart = function( event ) {
        if ( event.touches && event.touches.length == 2 ) {
            self.sizeToFit( event );
        } else {
            document.documentElement.addEvent('touchmove', self.drag);
            document.documentElement.addEvent('touchend', self.dragStop);
        }
        return KEYBOARD_LISTENER.stopEvent( event );
    };
    
    /**
     * DragStop of the grouptree divider to remove previously added document event listener
     * @param   {object}  event the event object
     * @returns {boolean} this is always fals to prevent further actions
     */
    this.dragStop = function( event ) {
        document.documentElement.removeEvent('mousemove', self.drag);
        document.documentElement.removeEvent('mouseup', self.dragStop);
        document.documentElement.removeEvent('touchmove', self.drag);
        document.documentElement.removeEvent('touchend', self.dragStop);
        self.showTree(true);  // Set the correct with
        $.jStorage.set('groupTreeWidth', self.grouptreeWrapper.style.width);
        return KEYBOARD_LISTENER.stopEvent( event );
    };
    
    /**
     * Drag-Function of the grouptree divider
     * @param   {object}  event the event object
     * @returns {boolean} this is always fals to prevent further actions
     */
    this.drag = function( event ) {
        var newWidth = event.pageX - parseInt(self.grouptreeWrapper.offsetLeft);
        self.grouptreeWrapper.style.width = Math.max(200, newWidth) + 'px'; // No less than 200px;
        return KEYBOARD_LISTENER.stopEvent( event );
    };
    
    /**
     * Resize group tree to fit content. This is the double click handler for the devider bar
     * @param   {object}  event the event object
     * @returns {boolean} this is always fals to prevent further actions
     */
    this.sizeToFit = function( event ) {
        self.grouptree.addClassName('minimize');
        var newWidth = self.grouptree.offsetWidth + 10; // 10px for a little bit of spacing to the right.
        self.grouptree.removeClassName('minimize');
        self.grouptreeWrapper.style.width = Math.max(200, newWidth) + 'px'; // No less than 200px;
        return self.dragStop( event ); // Set and save the size for the content area
    };
    
    /**
     * Recuse the grouptreedata and generate a shadow with the 'open' state of the nodes.
     * @param   {object} [currentNode=getPageCount.DATA.grouptreeData] the nodes to iterate over. Has to be groupTreeData from getPageCount
     * @returns {object} List of open paths in the group tree denoted by the url
     */
    this.getCurrentOpenTreeStructure = function( currentNode ) {
        
        currentNode = currentNode || (getPageCount.DATA ? getPageCount.DATA.grouptreeData : [] ) || []; /* safeguard the DATA variable. */
        var structure = {};
        
        currentNode.forEach(function(element){
            if ( !(element && element.itemData ) ) { return; }
            if ( element.itemData.parentNode.hasClassName('open') ) {
                structure[element.url] = self.getCurrentOpenTreeStructure( element.childs );
            }
        });
        
        return structure;
    };
    
    // Make singleton
    if (_grouptree == null) {
        _grouptree = self;
        return self.shouldInit ? self.init() : self;
    } else {
        return self;
    }
};