/*******************************************************************************
* 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;
}
};