/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.gui.components.tree; import static org.olat.core.gui.components.velocity.VelocityContainer.COMMAND_ID; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.olat.core.gui.UserRequest; import org.olat.core.gui.Windows; import org.olat.core.gui.components.AbstractComponent; import org.olat.core.gui.components.ComponentEventListener; import org.olat.core.gui.components.ComponentRenderer; import org.olat.core.gui.components.Window; import org.olat.core.gui.components.tree.InsertionPoint.Position; import org.olat.core.gui.control.JSAndCSSAdder; import org.olat.core.gui.control.winmgr.JSCommand; import org.olat.core.gui.render.ValidationResult; import org.olat.core.util.StringHelper; import org.olat.core.util.nodes.INode; import org.olat.core.util.tree.INodeFilter; /** * Description: <br> * * @author Felix Jost */ public class MenuTree extends AbstractComponent { private static final ComponentRenderer RENDERER = new MenuTreeRenderer(); /** * Comment for <code>NODE_IDENT</code> */ public static final String NODE_IDENT = "nidle"; /** * Comment for <code>NODE_IDENT</code> */ public static final String TARGET_NODE_IDENT = "tnidle"; /** * Comment for <code>NODE_IDENT</code> */ public static final String SIBLING_NODE = "sne"; /** * Comment for <code>NODE_IDENT</code> */ public static final String COMMAND_TREENODE = "ctntr"; /** * Comment for <code>NODE_IDENT</code> */ public static final String TREENODE_OPEN = "open"; /** * Comment for <code>NODE_IDENT</code> */ public static final String TREENODE_CLOSE = "close"; /** * event fired when a treenode was clicked (all leaf nodes) */ public static final String COMMAND_TREENODE_CLICKED = "ctncl"; /** * Command to insert new in the tree */ public static final String COMMAND_TREENODE_INSERT_UP = "iup"; public static final String COMMAND_TREENODE_INSERT_DOWN = "idown"; public static final String COMMAND_TREENODE_INSERT_UNDER = "iunder"; public static final String COMMAND_TREENODE_INSERT_REMOVE = "irm"; /** * event fired when a treenode was expanded (all nodes except leafs) */ public static final String COMMAND_TREENODE_EXPANDED = "ctnex"; /** * event fired when a treenode is dropper */ public static final String COMMAND_TREENODE_DROP = "ctdrop"; protected static final DefaultFilter DEF_FILTER = new DefaultFilter(); private TreeModel treeModel; private InsertionPoint insertionPoint; private String selectedNodeId = null; private final Set<String> selectedNodeIds = new HashSet<>(); private final Set<String> openNodeIds = new HashSet<>(); private boolean expandServerOnly = true; // default is serverside menu private boolean dragEnabled = false; private boolean dropEnabled = false; private boolean dropSiblingEnabled = false; private boolean expandSelectedNode = true; private boolean rootVisible = true; private boolean unselectNodes; private boolean showInsertTool; private boolean multiSelect; private boolean scrollTopOnClick; private String dndAcceptJSMethod = "treeAcceptDrop_notWithChildren"; private boolean dirtyForUser = false; private INodeFilter filter = DEF_FILTER; private MenuTreeItem menuTreeItem; /** * @param name */ public MenuTree(String name) { super(null, name); } public MenuTree(String id, String name) { super(id, name); } /** * @param id Fix unique identifier for state-less behavior * @param name * @param eventListener */ public MenuTree(String id, String name, ComponentEventListener eventListener) { super(id, name); addListener(eventListener); } MenuTree(String id, String name, ComponentEventListener eventListener, MenuTreeItem menuTreeItem) { this(id, name, eventListener); this.menuTreeItem = menuTreeItem; } @Override public void validate(UserRequest ureq, ValidationResult vr) { super.validate(ureq, vr); if(isDragEnabled() || isDropEnabled() || isDropSiblingEnabled()) { JSAndCSSAdder jsa = vr.getJsAndCSSAdder(); jsa.addRequiredStaticJsFile("js/jquery/ui/jquery-ui-1.11.4.custom.dnd.min.js"); } } private void scrollTop(UserRequest ureq) { Window window = Windows.getWindows(ureq).getWindow(ureq); if(window != null) { StringBuilder sb = new StringBuilder(); sb.append("try{ o_scrollToElement('#o_top'); }catch(e){}"); JSCommand jsCommand = new JSCommand(sb.toString()); window.getWindowBackOffice().sendCommandTo(jsCommand); } } /** * @see org.olat.core.gui.components.Component#dispatchRequest(org.olat.core.gui.UserRequest) */ @Override protected void doDispatchRequest(UserRequest ureq) { String cmd = ureq.getParameter(COMMAND_ID); String nodeId = ureq.getParameter(NODE_IDENT); if(COMMAND_TREENODE_CLICKED.equals(cmd)) { String openClose = ureq.getParameter(COMMAND_TREENODE); if(!StringHelper.containsNonWhitespace(openClose)) { boolean unselect = isUnselectNodes() && isSelectedOrDescendant(nodeId); if(unselect) { handleDeselect(nodeId); } else { selectedNodeId = nodeId; } } handleClick(ureq, openClose, nodeId); } else if (COMMAND_TREENODE_DROP.equals(cmd)) { String targetNodeId = ureq.getParameter(TARGET_NODE_IDENT); String sneValue = ureq.getParameter(SIBLING_NODE); boolean sibling = StringHelper.containsNonWhitespace(sneValue); boolean atTheEnd = "end".equals(sneValue); handleDropped(ureq, targetNodeId, nodeId, sibling, atTheEnd); } else if(COMMAND_TREENODE_INSERT_UP.equals(cmd)) { insertionPoint = new InsertionPoint(nodeId, InsertionPoint.Position.up); setDirty(true); fireEvent(ureq, new InsertEvent(COMMAND_TREENODE_INSERT_UP)); } else if(COMMAND_TREENODE_INSERT_DOWN.equals(cmd)) { insertionPoint = new InsertionPoint(nodeId, InsertionPoint.Position.down); setDirty(true); fireEvent(ureq, new InsertEvent(COMMAND_TREENODE_INSERT_DOWN)); } else if(COMMAND_TREENODE_INSERT_UNDER.equals(cmd)) { insertionPoint = new InsertionPoint(nodeId, InsertionPoint.Position.under); setDirty(true); fireEvent(ureq, new InsertEvent(COMMAND_TREENODE_INSERT_UNDER)); } else if(COMMAND_TREENODE_INSERT_REMOVE.equals(cmd)) { insertionPoint = null; setDirty(true); fireEvent(ureq, new InsertEvent(COMMAND_TREENODE_INSERT_REMOVE)); } } /** * this is true when the user expanded a treenode to view its children. * it is false when the user clicked on a node with an action */ @Override public boolean isDirtyForUser() { return dirtyForUser; } @Override public void setDirty(boolean dirty) { super.setDirty(dirty); if (!dirty) { // clear the userdirty flag also dirtyForUser = false; } } // -- recorder methods private void handleDropped(UserRequest ureq, String droppedNodeId, String targetNodeId, boolean sibling, boolean atTheEnd) { TreeDropEvent te = new TreeDropEvent(COMMAND_TREENODE_DROP, droppedNodeId, targetNodeId, !sibling, atTheEnd); fireEvent(ureq, te); super.setDirty(true); } private void handleDeselect(String nodeId) { TreeNode node = treeModel.getNodeById(nodeId); INode parentNode = node.getParent(); if(parentNode != null) { setSelectedNodeId(parentNode.getIdent()); } else { clearSelection(); } } /** * @param selTreeNode */ private void handleClick(UserRequest ureq, String cmd, String selNodeId) { TreeNode selTreeNode = treeModel.getNodeById(selNodeId); // could be if upon click, an error occured -> timestamp check does not apply, but the tree model was regenerated (as in course) if (selTreeNode == null) return; if (!selTreeNode.isAccessible()){ TreeEvent te = new TreeEvent(COMMAND_TREENODE_EXPANDED, selNodeId); dirtyForUser = true; super.setDirty(true); fireEvent(ureq, te); return; } TreeNode deleg = selTreeNode.getDelegate(); if (deleg != null) { updateOpenedNode(selTreeNode, selNodeId, cmd); selNodeId = deleg.getIdent(); selTreeNode = deleg; } String subCmd = null; if(TREENODE_CLOSE.equals(cmd)) { subCmd = TreeEvent.COMMAND_TREENODE_CLOSE; } else if (TREENODE_OPEN.equals(cmd)) { subCmd = TreeEvent.COMMAND_TREENODE_OPEN; } else { scrollTop(ureq); } updateOpenedNode(selTreeNode, selNodeId, cmd); TreeEvent te = new TreeEvent(COMMAND_TREENODE_CLICKED, subCmd, selNodeId); if (selTreeNode.getChildCount() > 0) { dirtyForUser = true; } // else dirtyForUser is false, since we clicked a node (which only results in the node beeing marked in a visual style) super.setDirty(true); fireEvent(ureq, te); } private boolean isSelectedOrDescendant(String nodeId) { if(nodeId.equals(getSelectedNodeId())) { return true; } return false; } private boolean updateOpenedNode(TreeNode treeNode, String nodeId, String cmd) { if(TREENODE_CLOSE.equals(cmd)) { removeTreeNodeFromOpenList(treeNode, nodeId); if(selectedNodeId != null && isChildOf(treeNode, selectedNodeId)) { clearSelection(); setSelectedNodeId(nodeId); return true; } } else if (TREENODE_OPEN.equals(cmd)) { openNodeIds.add(nodeId); if(treeNode.getUserObject() instanceof String) { openNodeIds.add((String)treeNode.getUserObject()); } } else if (cmd == null) { openNodeIds.add(nodeId); if(treeNode.getUserObject() instanceof String) { openNodeIds.add((String)treeNode.getUserObject()); } } return false; } private void removeTreeNodeFromOpenList(TreeNode treeNode, String nodeId) { openNodeIds.remove(nodeId); openNodeIds.remove(treeNode.getUserObject()); for(int i=treeNode.getChildCount(); i-->0; ) { TreeNode child = (TreeNode)treeNode.getChildAt(i); String childId = child.getIdent(); TreeNode deleg = child.getDelegate(); if (deleg != null) { childId = deleg.getIdent(); child = deleg; } removeTreeNodeFromOpenList(child, childId); } } private boolean isChildOf(INode treeNode, String childId) { int childCount = treeNode.getChildCount(); for(int i=0; i<childCount; i++) { INode childNode = treeNode.getChildAt(i); if(childNode.getIdent().equals(childId) || (childNode instanceof TreeNode && childId.equals(((TreeNode)childNode).getUserObject())) || (isChildOf(childNode, childId))) { return true; } } return false; } /** * @return the selected node */ public TreeNode getSelectedNode() { return (selectedNodeId == null ? null : treeModel.getNodeById(selectedNodeId)); } /** * @return the selected node's id */ public String getSelectedNodeId() { return selectedNodeId; } /** * @param nodeId */ public void setSelectedNodeId(String nodeId) { selectedNodeId = nodeId; setDirty(true); } public Set<String> getSelectedNodeIds() { return selectedNodeIds; } public void setSelectedNodeIds(Collection<String> newSelectedNodeIds) { selectedNodeIds.clear(); selectedNodeIds.addAll(newSelectedNodeIds); } public boolean isSelected(TreeNode node) { return node != null && selectedNodeIds.contains(node.getIdent()); } public void select(String id, boolean select) { if(select) { selectedNodeIds.add(id); } else { selectedNodeIds.remove(id); } } public boolean isOpen(TreeNode node) { return openNodeIds.contains(node.getIdent()); } public void open(TreeNode node) { for(INode iteratorNode=node; node.getParent() != null && iteratorNode != null && !openNodeIds.contains(iteratorNode.getIdent()); iteratorNode=iteratorNode.getParent()) { openNodeIds.add(iteratorNode.getIdent()); } } public Collection<String> getOpenNodeIds() { return openNodeIds; } public void setOpenNodeIds(Collection<String> nodeIds) { openNodeIds.clear(); if(nodeIds != null) { openNodeIds.addAll(nodeIds); } setDirty(true); } public void clearSelection() { selectedNodeId = null; } public MenuTreeItem getMenuTreeItem() { return menuTreeItem; } public InsertionPoint getInsertionPoint() { return insertionPoint; } public TreePosition getInsertionPosition() { if(insertionPoint == null) return null; int position; TreeNode parent; TreeNode node = treeModel.getNodeById(insertionPoint.getNodeId()); if(insertionPoint.getPosition() == Position.under) { parent = node; position = 0; } else if(insertionPoint.getPosition() == Position.up) { parent = (TreeNode)node.getParent(); position = node.getPosition(); } else if(insertionPoint.getPosition() == Position.down) { parent = (TreeNode)node.getParent(); position = node.getPosition() + 1; } else { return null; } return new TreePosition(parent, position); } /** * @return MutableTreeModel */ public TreeModel getTreeModel() { return treeModel; } /** * Sets the treeModel. * * @param treeModel The treeModel to set */ public void setTreeModel(TreeModel treeModel) { this.treeModel = treeModel; selectedNodeId = null; // clear selection if a new model is set dirtyForUser = true; super.setDirty(true); } /** * @return Returns the expandServerOnly. */ public boolean isExpandServerOnly() { return expandServerOnly; } /** * @param expandServerOnly The expandServerOnly to set. */ public void setExpandServerOnly(boolean expandServerOnly) { this.expandServerOnly = expandServerOnly; } public boolean isInsertToolEnabled() { return showInsertTool; } /** * Use the insert tool * @param enableInsertTool */ public void enableInsertTool(boolean enableInsertTool) { showInsertTool = enableInsertTool; } protected boolean isMultiSelect() { return multiSelect; } protected void setMultiSelect(boolean multiSelect) { this.multiSelect = multiSelect; } public INodeFilter getFilter() { return filter; } public void setFilter(INodeFilter filter) { if(filter == null) { this.filter = DEF_FILTER; } else { this.filter = filter; } } public boolean isScrollTopOnClick() { return scrollTopOnClick; } public void setScrollTopOnClick(boolean scrollTopOnClick) { this.scrollTopOnClick = scrollTopOnClick; } public boolean isDragEnabled() { return dragEnabled; } /** * @param enableDragAndDrop Enable or not drag and drop */ public void setDragEnabled(boolean enabled) { dragEnabled = enabled; } /** * @return Is Drag & Drop enable for the tree */ public boolean isDropEnabled() { return dropEnabled; } public void setDropEnabled(boolean enabled) { dropEnabled = enabled; } public boolean isDropSiblingEnabled() { return dropSiblingEnabled; } public void setDropSiblingEnabled(boolean enabled) { dropSiblingEnabled = enabled; } public String getDndAcceptJSMethod() { return dndAcceptJSMethod; } public void setDndAcceptJSMethod(String dndAcceptJSMethod) { this.dndAcceptJSMethod = dndAcceptJSMethod; } /** * Expand the selected node to view its children * @return */ public boolean isExpandSelectedNode() { return expandSelectedNode; } public void setExpandSelectedNode(boolean expandSelectedNode) { this.expandSelectedNode = expandSelectedNode; } public boolean isUnselectNodes() { return unselectNodes; } public void setUnselectNodes(boolean unselectNodes) { this.unselectNodes = unselectNodes; } /** * The root node is visible per default * @return */ public boolean isRootVisible() { return rootVisible; } public void setRootVisible(boolean rootVisible) { this.rootVisible = rootVisible; } /** * @param nodeForum */ public void setSelectedNode(TreeNode node) { if(node == null) { setSelectedNodeId(null); } else { String nId = node.getIdent(); setSelectedNodeId(nId); } } public ComponentRenderer getHTMLRendererSingleton() { return RENDERER; } private static class DefaultFilter implements INodeFilter { @Override public boolean isVisible(INode node) { return true; } } }