import Soy from 'metal-soy';
import Treeview from 'frontend-js-web/liferay/compat/treeview/Treeview.es';
import core from 'metal';
import dom from 'metal-dom';
import { Config } from 'metal-state';
import templates from './CardsTreeview.soy';
/**
* CardsTreeview
*
* This is an extension of the default TreeView component that adds
* the following features:
*
* - Node selection management, both single and multiple
* - Custom tree node template using Lexicon Horizontal Cards
* - Improved accessibility for keyboard navigation following common tree widget patterns
* @review
*/
class CardsTreeview extends Treeview {
/**
* @inheritDoc
* @review
*/
created() {
this.expandSelectedNodesParentNodes_(this.nodes);
this.addSelectedNodes_(this.nodes);
}
/**
* Adds nodes with selected attribute to selectedNodes list in case when
* they are still not there.
* @param nodes Nodes to check and add to selectedNodes list.
* @protected
* @review
*/
addSelectedNodes_(nodes) {
nodes.forEach(
(node) => {
if (node.children) {
this.addSelectedNodes_(node.children);
}
if (node.selected) {
this.selectNode_(node);
}
}
);
}
/**
* Deselects all selected tree nodes
* @protected
* @review
*/
deselectAll_() {
for (let i = this.selectedNodes.length - 1; i >= 0; i--) {
this.selectedNodes[i].selected = false;
this.selectedNodes.pop();
}
}
/**
* Selects specific nodes
* @param node to deselect.
* @protected
* @review
*/
deselectNode_(node) {
node.selected = false;
this.selectedNodes.splice(this.selectedNodes.indexOf(node), 1);
this.selectedNodes = this.selectedNodes;
}
/**
* Expands all parent nodes of expanded children.
* @param nodes List of nodes to expand all parent nodes of expanded children.
* @protected
* @review
*/
expandSelectedNodesParentNodes_(nodes) {
let expanded,
expandedParent;
nodes.forEach(
(node) => {
expanded = node.expanded;
if (node.selected) {
expandedParent = true;
}
if (node.children) {
expanded = this.expandSelectedNodesParentNodes_(node.children) || expanded;
}
node.expanded = expanded;
},
this
);
return expandedParent;
}
/**
* Focus the given tree node.
* @param {!Object} nodeObj
* @protected
* @review
*/
focus_(nodeObj) {
if (nodeObj) {
this.element.querySelector('[data-treeitemid="' + nodeObj.id + '"] .card').focus();
}
}
/**
* Focus the next tree node of given tree node.
* @param {!Element} node
* @protected
* @review
*/
focusNextNode_(node) {
let path = node.getAttribute('data-treeview-path').split('-');
let nodeObj = this.getNodeObj(path);
let nextNodeObj;
if (nodeObj.children && nodeObj.expanded) {
path.push(0);
nextNodeObj = this.getNodeObj(path);
}
else {
while (!nextNodeObj && path.length > 0) {
path[path.length - 1]++;
nextNodeObj = this.getNodeObj(path);
path.pop();
}
}
this.focus_(nextNodeObj);
}
/**
* Focus the previous tree node of given tree node.
* @param {!Element} node
* @protected
* @review
*/
focusPrevNode_(node) {
let path = node.getAttribute('data-treeview-path').split('-');
let prevNodeObj;
if (path[path.length - 1] === '0') {
path.pop();
prevNodeObj = this.getNodeObj(path);
}
else {
path[path.length - 1]--;
prevNodeObj = this.getNodeObj(path);
while (prevNodeObj.children && prevNodeObj.expanded) {
prevNodeObj = prevNodeObj.children[prevNodeObj.children.length - 1];
}
}
this.focus_(prevNodeObj);
}
/**
* This is called when one of this tree view's nodes is clicked.
* @param {!Event} event
* @protected
* @review
*/
handleNodeClicked_(event) {
let path = event.delegateTarget.parentNode.parentNode.parentNode.getAttribute('data-treeview-path').split('-');
let node = this.getNodeObj(path);
if (this.multiSelection) {
if (node.selected) {
this.deselectNode_(node);
}
else {
this.selectNode_(node);
}
}
else {
if (!node.selected) {
this.deselectAll_();
this.selectNode_(node);
}
}
this.nodes = this.nodes;
}
/**
* This is called when one of this tree view's nodes receives a keypress.
* Depending on the pressed key, the tree will:
* - ENTER or SPACE: Select the current node
* - DOWN ARROW: Focus the next node
* - UP ARROW: Focus the previous node
* - LEFT ARROW: Collapse the current node
* - RIGHT ARROW: Expand the current node
* @param {!Event} event
* @protected
* @review
*/
handleNodeKeyUp_(event) {
let node = event.delegateTarget.parentNode.parentNode.parentNode;
if (event.keyCode === 37) {
this.setNodeExpandedState_(node, {expanded: false});
}
else if (event.keyCode === 38) {
this.focusPrevNode_(node);
}
else if (event.keyCode === 39) {
this.setNodeExpandedState_(node, {expanded: true});
}
else if (event.keyCode === 40) {
this.focusNextNode_(node);
}
else if (event.keyCode === 13 || event.keyCode === 32) {
this.handleNodeClicked_(event);
}
}
/**
* This is called when one of this tree view's nodes toggler is clicked.
* @param {!Event} event
* @protected
* @review
*/
handleNodeTogglerClicked_(event) {
this.toggleExpandedState_(event.delegateTarget.parentNode.parentNode.parentNode);
}
/**
* Selects specific node.
* @param node to select.
* @protected
* @review
*/
selectNode_(node) {
node.selected = true;
this.selectedNodes.push(node);
this.selectedNodes = this.selectedNodes;
}
/**
* Sets the expanded state of a node
* @param {!Element} node The tree node we want to change the expanded state to
* @param {!Object} state A state object with the new value of the expanded state
* @protected
* @review
*/
setNodeExpandedState_(node, state) {
let path = node.getAttribute('data-treeview-path').split('-');
let nodeObj = this.getNodeObj(path);
nodeObj.expanded = state.expanded;
this.nodes = this.nodes;
}
}
/**
* CardsTreeview state definition.
* @type {!Object}
* @review
* @static
*/
CardsTreeview.STATE = {
/**
* Enables multiple selection of tree elements
* @review
* @type {boolean}
*/
multiSelection: Config.bool().value(false),
/**
* List of selected nodes
* @review
* @type {Array.<Object>}
*/
selectedNodes: Config.array().value([]),
/**
* Type of view to render. Accepted values are 'tree' and 'flat'
* @review
* @type {String}
*/
viewType: Config.string().value('tree')
};
Soy.register(CardsTreeview, templates);
export default CardsTreeview;