/** * 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.tree.MenuTree.COMMAND_TREENODE; import static org.olat.core.gui.components.tree.MenuTree.COMMAND_TREENODE_CLICKED; import static org.olat.core.gui.components.tree.MenuTree.COMMAND_TREENODE_DROP; import static org.olat.core.gui.components.tree.MenuTree.COMMAND_TREENODE_INSERT_DOWN; import static org.olat.core.gui.components.tree.MenuTree.COMMAND_TREENODE_INSERT_REMOVE; import static org.olat.core.gui.components.tree.MenuTree.COMMAND_TREENODE_INSERT_UNDER; import static org.olat.core.gui.components.tree.MenuTree.COMMAND_TREENODE_INSERT_UP; import static org.olat.core.gui.components.tree.MenuTree.NODE_IDENT; import static org.olat.core.gui.components.velocity.VelocityContainer.COMMAND_ID; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.commons.lang.StringEscapeUtils; import org.olat.core.gui.components.Component; import org.olat.core.gui.components.DefaultComponentRenderer; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormJSHelper; import org.olat.core.gui.components.form.flexible.impl.NameValuePair; import org.olat.core.gui.components.tree.InsertionPoint.Position; import org.olat.core.gui.control.winmgr.AJAXFlags; import org.olat.core.gui.render.RenderResult; import org.olat.core.gui.render.Renderer; import org.olat.core.gui.render.StringOutput; import org.olat.core.gui.render.URLBuilder; import org.olat.core.gui.translator.Translator; import org.olat.core.util.StringHelper; import org.olat.core.util.Util; import org.olat.core.util.nodes.INode; /** * enclosing_type Description: <br> * * @author Felix Jost, Florian Gnaegi */ public class MenuTreeRenderer extends DefaultComponentRenderer { /** * Constructor for TableRenderer. Singleton and must be reentrant There must * be an empty contructor for the Class.forName() call */ public MenuTreeRenderer() { super(); } /** * @see org.olat.core.gui.render.ui.ComponentRenderer#render(org.olat.core.gui.render.Renderer, * org.olat.core.gui.render.StringOutput, org.olat.core.gui.components.Component, * org.olat.core.gui.render.URLBuilder, org.olat.core.gui.translator.Translator, * org.olat.core.gui.render.RenderResult, java.lang.String[]) */ @Override public void render(Renderer renderer, StringOutput target, Component source, URLBuilder ubu, Translator translator, RenderResult renderResult, String[] args) { MenuTree tree = (MenuTree) source; if(tree.getMenuTreeItem() != null) { tree.getMenuTreeItem().clearVisibleNodes(); } TreeNode root = tree.getTreeModel().getRootNode(); if (root == null) return; // tree is completely empty INode selNode = tree.getSelectedNode(); Collection<String> openNodeIds = tree.getOpenNodeIds(); List<INode> selPath = new ArrayList<INode>(5); INode cur = selNode; if (cur == null && !tree.isUnselectNodes()) { cur = root; } else if(cur == null && tree.isUnselectNodes() && openNodeIds.isEmpty()) { openNodeIds.add(root.getIdent());//always open the root } // if no selection, select the first node to // expand the children // add all elems from selected path to reversed list -> first elem is // selected nodeid of the root node while (cur != null) { selPath.add(0, cur); cur = cur.getParent(); } List<DndElement> elements = new ArrayList<>(); AJAXFlags flags = renderer.getGlobalSettings().getAjaxFlags(); target.append("\n<div class='o_tree"); // marker classes to differentiate rendering when root node is visible if(!tree.isRootVisible()) { target.append(" o_tree_root_hidden"); } else { target.append(" o_tree_root_visible"); } if(tree.isInsertToolEnabled()) { target.append(" o_tree_insert_tool"); } // add element CSS if(StringHelper.containsNonWhitespace(tree.getElementCssClass())) { target.append(" ").append(tree.getElementCssClass()); } target.append("'><ul class=\"o_tree_l0\">"); if(tree.isRootVisible()) { renderLevel(target, 0, root, selPath, openNodeIds, elements, ubu, flags, tree); } else { selPath.remove(0); int chdCnt = root.getChildCount(); for (int i = 0; i < chdCnt; i++) { TreeNode curChd = (TreeNode)root.getChildAt(i); renderLevel(target, 0, curChd, selPath, openNodeIds, elements, ubu, flags, tree); } } target.append("</ul>"); appendDragAndDropScript(elements, tree, target) .append("</div>"); } private void renderLevel(StringOutput target, int level, TreeNode curRoot, List<INode> selPath, Collection<String> openNodeIds, List<DndElement> dndElements, URLBuilder ubu, AJAXFlags flags, MenuTree tree) { INode curSel = null; if (level < selPath.size()) { curSel = selPath.get(level); } boolean selected = (!selPath.isEmpty() && selPath.get(selPath.size() - 1) == curRoot); boolean hasInsertionPoint = isInsertionPointUnderNode(curRoot, tree); boolean hasChildren = hasVisibleChildren(curRoot, tree); boolean renderChildren = isRenderChildren(curSel, curRoot, selected, tree, openNodeIds) || hasInsertionPoint; renderInsertionPoint(target, Position.up, level, curRoot, ubu, flags, tree); // item icon css class and icon decorator (for each icon quadrant a div, eclipse style) String cssClass = curRoot.getCssClass(); target.append("<li class='"); // add custom css class target.append(cssClass, cssClass != null); if(selected) { target.append(" active"); } else if (curSel == curRoot) { // add css class to identify parents of active element target.append(" active_parent"); } String ident = curRoot.getIdent(); target.append("' data-nodeid='").append(ident).append("'>"); target.append("<div id='dd").append(ident).append("' class='o_tree_l").append(level); if(tree.isDragEnabled() || tree.isDropEnabled()) { target.append(" o_dnd_item"); } if(selected) { target.append(" active"); } target.append("'>"); if(tree.isDragEnabled() || tree.isDropEnabled()) { appendDragAndDropElement(curRoot, tree, dndElements, ubu, flags); } if(hasChildren || hasInsertionPoint) { renderOpenClose(curRoot, target, level, renderChildren, ubu, flags, tree); } // Render menu item as link, also for active elements // mark active item as strong for accessibility reasons renderLink(curRoot, level, selected, renderChildren, curSel, target, ubu, flags, tree); if(selected && tree.isInsertToolEnabled()) { renderInsertCallout(target, curRoot, ubu, flags, tree); } target.append("</div>"); //append div to drop as sibling if(!renderChildren && (tree.isDragEnabled() || tree.isDropSiblingEnabled())) { appendSiblingDropObj(curRoot, level, tree, target, false); } if (renderChildren) { //open / close ul renderChildren(target, level, curRoot, selPath, openNodeIds, dndElements, ubu, flags, tree); //append div to drop as sibling after the children if(tree.isDragEnabled() || tree.isDropSiblingEnabled()) { appendSiblingDropObj(curRoot, level, tree, target, true); } } // close item level target.append("</li>"); renderInsertionPoint(target, Position.down, level, curRoot, ubu, flags, tree); } private void renderCheckbox(StringOutput sb, TreeNode node, MenuTree tree) { MenuTreeItem treeItem = tree.getMenuTreeItem(); if(treeItem != null) { boolean enabled = treeItem.isEnabled(); boolean selected = tree.isSelected(node); boolean intermediate = treeItem.isIndeterminate(node); String groupingName = "tcb_ms"; String id = "cb" + node.getIdent(); sb.append("<input type='checkbox' id='").append(id).append("' ") .append(" name='").append(groupingName).append("'") .append(" value='").append(node.getIdent()).append("'"); if (selected) { sb.append(" checked='checked' "); } if(enabled){ //use the selection form dispatch id and not the one of the element! sb.append(FormJSHelper.getRawJSFor(treeItem.getRootForm(), treeItem.getFormDispatchId(), FormEvent.ONCLICK)); } else { sb.append(" disabled='disabled' "); } sb.append(" />"); if(intermediate) { sb.append("<script type='text/javascript'>\n") .append("/* <![CDATA[ */\n") .append("jQuery(function() {\n") .append(" jQuery('#").append(id).append("').prop('indeterminate', true);") .append("});\n") .append("/* ]]> */") .append("</script>\n"); } } } private void renderInsertionPoint(StringOutput sb, Position positionToRender, int level, TreeNode node, URLBuilder ubu, AJAXFlags flags, MenuTree tree) { if(tree.getInsertionPoint() != null && tree.getInsertionPoint().getPosition() == positionToRender && tree.getInsertionPoint().getNodeId().equals(node.getIdent())) { sb.append("<li><div class='o_tree_l").append(level).append("'>") .append("<span class=\"o_tree_leaf o_tree_oc_l").append(level).append("\"> </span>") .append("<span class='o_tree_link o_tree_l").append(level).append(" o_insertion_point'><a href=\""); ubu.buildHrefAndOnclick(sb, null, flags.isIframePostEnabled(), false, false, new NameValuePair(COMMAND_ID, COMMAND_TREENODE_INSERT_REMOVE), new NameValuePair(NODE_IDENT, node.getIdent())); Translator pointTranslator = Util.createPackageTranslator(MenuTreeRenderer.class, tree.getTranslator().getLocale()); String pointTranslation = pointTranslator.translate("insertion.point"); sb.append("><span>").append(pointTranslation).append(" <i class='o_icon o_icon_remove'> </i></span>") .append("</a></span></div></li>"); } } private void renderOpenClose(TreeNode curRoot, StringOutput target, int level, boolean renderChildren, URLBuilder ubu, AJAXFlags flags, MenuTree tree) { int chdCnt = curRoot.getChildCount(); // expand icon // add ajax support and real open/close function if (((tree.isRootVisible() && level != 0) || !tree.isRootVisible()) && chdCnt > 0) { // root has not open/close icon, append open / close icon only if there is children target.append("<a "); ubu.buildHrefAndOnclick(target, null, flags.isIframePostEnabled(), tree.getMenuTreeItem() != null, true, new NameValuePair(COMMAND_ID, COMMAND_TREENODE_CLICKED), new NameValuePair(NODE_IDENT, curRoot.getIdent()), new NameValuePair(COMMAND_TREENODE, renderChildren ? MenuTree.TREENODE_CLOSE : MenuTree.TREENODE_OPEN)); String openCloseCss = renderChildren ? "close" : "open"; target.append(" class='o_tree_oc_l").append(level).append("'><i class='o_icon o_icon_").append(openCloseCss).append("_tree'></i></a>"); } else if (level != 0 && chdCnt == 0) { target.append("<span class=\"o_tree_leaf o_tree_oc_l").append(level).append("\"> </span>"); } } private void renderLink(TreeNode curRoot, int level, boolean selected, boolean renderChildren, INode curSel, StringOutput target, URLBuilder ubu, AJAXFlags flags, MenuTree tree) { int chdCnt = curRoot.getChildCount(); boolean iframePostEnabled = flags.isIframePostEnabled(); if(tree.getMenuTreeItem() != null) { tree.getMenuTreeItem().trackVisibleNode(curRoot); } target.append("<span class='o_tree_link o_tree_l").append(level); // add icon css class if (selected) { // add css class to identify active element target.append(" active"); } else if (curSel == curRoot) { // add css class to identify parents of active element target.append(" active_parent"); } boolean insertionSource = (tree.getTreeModel() instanceof InsertionTreeModel && ((InsertionTreeModel)tree.getTreeModel()).isSource(curRoot)); if(insertionSource) { target.append(" o_insertion_source"); } //reapply the same rules to the second link if(level != 0 && chdCnt > 0) { if (renderChildren) { target.append(" o_tree_level_label_close"); } else { target.append(" o_tree_level_label_open"); } } else if (level != 0 && chdCnt == 0) { target.append(" o_tree_level_label_leaf"); } target.append("'>"); if(tree.isMultiSelect() && tree.getMenuTreeItem() != null) { renderCheckbox(target, curRoot, tree); } // Build menu item URI target.append("<a "); boolean dirtyCheck = tree.getMenuTreeItem() == null || !tree.getMenuTreeItem().isNoDirtyCheckOnClick(); ubu.buildHrefAndOnclick(target, null, iframePostEnabled, dirtyCheck, true, new NameValuePair(COMMAND_ID, COMMAND_TREENODE_CLICKED), new NameValuePair(NODE_IDENT, curRoot.getIdent())); // Add menu item title as alt hoover text String alt = curRoot.getAltText(); if (alt != null) { target.append(" title=\"") .append(StringEscapeUtils.escapeHtml(alt).toString()) .append("\""); } target.append(">"); String iconCssClass = curRoot.getIconCssClass(); if (iconCssClass != null) { target.append("<i class='o_icon ").append(iconCssClass).append("'></i> "); } renderDisplayTitle(target, curRoot, tree); // display title and close menu item appendDecorators(curRoot, target); target.append("</a></span>"); } private void renderDisplayTitle(StringOutput target, TreeNode node, MenuTree tree) { target.append("<span "); if(tree.isDragEnabled() || tree.isDropEnabled()) { if(tree.isDragEnabled()) { target.append(" class='o_dnd_item'"); } else { target.append(" class='o_tree_item'"); } target.append(" id='da").append(node.getIdent()).append("'"); } else { target.append(" class='o_tree_item'"); } target.append(">"); // render link String title = node.getTitle(); if(title != null && title.equals("")) { target.append(" "); } else { StringHelper.escapeHtml(target, title); } target.append("</span>"); } private void renderInsertCallout(StringOutput sb, TreeNode node, URLBuilder ubu, AJAXFlags flags, MenuTree tree) { Position[] positionArr; if(tree.getTreeModel() instanceof InsertionTreeModel) { positionArr = ((InsertionTreeModel)tree.getTreeModel()).getInsertionPosition(node); } else if(tree.getTreeModel().getRootNode() != node) { positionArr = new Position[] { Position.under }; } else { positionArr = new Position[] { Position.up, Position.down, Position.under }; } if(positionArr.length > 0) { sb.append("<div class='popover right show'>") .append("<div class='arrow'></div>") .append("<div class='popover-content btn-group'>"); if(Position.hasPosition(Position.up, positionArr)) { renderInsertCalloutButton(COMMAND_TREENODE_INSERT_UP, "o_icon_node_before", sb, node, ubu, flags); } if(Position.hasPosition(Position.down, positionArr)) { renderInsertCalloutButton(COMMAND_TREENODE_INSERT_DOWN, "o_icon_node_after", sb, node, ubu, flags); } if(Position.hasPosition(Position.under, positionArr)) { renderInsertCalloutButton(COMMAND_TREENODE_INSERT_UNDER, "o_icon_node_under o_icon-rotate-180", sb, node, ubu, flags); } sb.append("</div></div>"); } } private void renderInsertCalloutButton(String cmd, String cssClass, StringOutput sb, TreeNode node, URLBuilder ubu, AJAXFlags flags) { sb.append("<a class='btn btn-default small' "); ubu.buildHrefAndOnclick(sb, flags.isIframePostEnabled(), new NameValuePair(COMMAND_ID, cmd), new NameValuePair(NODE_IDENT, node.getIdent())); sb.append("><i class='o_icon ").append(cssClass).append("'> </i></a>"); } private void renderChildren(StringOutput target, int level, TreeNode curRoot, List<INode> selPath, Collection<String> openNodeIds, List<DndElement> dndElements, URLBuilder ubu, AJAXFlags flags, MenuTree tree) { int chdCnt = curRoot.getChildCount(); // render children as new level target.append("\n<ul class=\""); // add css class to identify level target.append(" o_tree_l").append(level + 1) .append("\">"); renderInsertionPoint(target, Position.under, level + 1, curRoot, ubu, flags, tree); // render all the nodes from this level for (int i = 0; i < chdCnt; i++) { TreeNode curChd = (TreeNode) curRoot.getChildAt(i); if(tree.getFilter().isVisible(curChd)) { renderLevel(target, level + 1, curChd, selPath, openNodeIds, dndElements, ubu, flags, tree); } } target.append("</ul>"); } private void appendSiblingDropObj(TreeNode node, int level, MenuTree tree, StringOutput target, boolean after) { boolean drop = tree.isDropEnabled() && ((DnDTreeModel)tree.getTreeModel()).isNodeDroppable(node); if(drop) { String id = (after ? "dt" : "ds") + node.getIdent(); target.append("<div id='").append(id).append("' class='o_dnd_sibling o_dnd_l").append(level).append("'> </div>"); } } private void appendDragAndDropElement(TreeNode node, MenuTree tree, List<DndElement> target, URLBuilder ubu, AJAXFlags flags) { String id = node.getIdent(); boolean drag = tree.isDragEnabled() && ((DnDTreeModel)tree.getTreeModel()).isNodeDraggable(node); boolean drop = tree.isDropEnabled() && ((DnDTreeModel)tree.getTreeModel()).isNodeDroppable(node); if(drag || drop) { DndElement el = new DndElement(); el.setId(id); if(drag) { el.setDrag(drag); } if(drop) { el.setDrop(true); StringOutput endUrl = new StringOutput(64); ubu.buildURI(endUrl, new String[] { COMMAND_ID, NODE_IDENT }, new String[] { COMMAND_TREENODE_DROP, node.getIdent() }, flags.isIframePostEnabled() ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL); el.setEndUrl(endUrl.toString()); } target.add(el); } } private StringOutput appendDragAndDropScript(List<DndElement> elements, MenuTree tree, StringOutput sb) { if(elements == null || elements.isEmpty()) return sb; sb.append("<script type='text/javascript'>\n") .append("/* <![CDATA[ */\n") .append("jQuery(function() {\n"); StringBuilder dragIds = new StringBuilder("["); StringBuilder dropIds = new StringBuilder("["); for(DndElement element:elements) { if(element.isDrag()) { if(dragIds.length() > 1) { dragIds.append(","); } dragIds.append("'").append(element.getId()).append("'"); } if(element.isDrop()) { if(dropIds.length() > 1) { dropIds.append(","); } dropIds.append("['").append(element.getId()).append("','").append(element.getEndUrl()).append("']"); } } dragIds.append("]"); dropIds.append("]"); if(dragIds.length() > 2) { sb.append(" jQuery.each(").append(dragIds).append(", function(index, value) {\n") .append(" jQuery('#da' + value)"); appendDraggable(sb).append("\n") .append(" jQuery('#dd' + value)"); appendDraggable(sb).append("\n") .append(" });\n"); } if(dropIds.length() > 2) { String acceptMethod = tree.getDndAcceptJSMethod(); if(acceptMethod == null) { acceptMethod = "treeAcceptDrop"; } sb.append(" jQuery.each(").append(dropIds).append(", function(index, value) {\n") .append(" jQuery('#dd' + value[0]).droppable({ endUrl:value[1],hoverClass:'o_dnd_over',").append("accept:").append(acceptMethod).append(",drop:onTreeDrop});\n"); if(tree.isDropSiblingEnabled()) { sb.append(" jQuery('#dt' + value[0]).droppable({ endUrl:value[1],hoverClass:'o_dnd_over',").append("accept:").append(acceptMethod).append(",drop:onTreeDrop});\n") .append(" jQuery('#ds' + value[0]).droppable({ endUrl:value[1],hoverClass:'o_dnd_over',").append("accept:").append(acceptMethod).append(",drop:onTreeDrop});\n"); } sb.append(" });\n"); } sb.append("});\n") .append("/* ]]> */") .append("</script>\n"); return sb; } private StringOutput appendDraggable(StringOutput sb) { sb.append(".draggable({start:onTreeStartDrag, stop: onTreeStopDrag, delay:100, distance:5, revert:'invalid' });"); return sb; } private static class DndElement { private String id; private String endUrl; private boolean drag,drop; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getEndUrl() { return endUrl; } public void setEndUrl(String endUrl) { this.endUrl = endUrl; } public boolean isDrag() { return drag; } public void setDrag(boolean drag) { this.drag = drag; } public boolean isDrop() { return drop; } public void setDrop(boolean drop) { this.drop = drop; } } private void appendDecorators(TreeNode node, StringOutput sb) { appendDecorator(node.getIconDecorator1CssClass(), sb, 1); appendDecorator(node.getIconDecorator2CssClass(), sb, 2); appendDecorator(node.getIconDecorator3CssClass(), sb, 3); appendDecorator(node.getIconDecorator4CssClass(), sb, 4); } private void appendDecorator(String decorator, StringOutput sb, int num) { if (decorator != null && decorator.length() > 0) { sb.append("<span class='badge o_badge_").append(num).append(" ").append(decorator).append("'><i class='o_icon ").append(decorator).append("'></i></span>"); } } private boolean isRenderChildren(INode curSel, TreeNode curRoot, boolean selected, MenuTree tree, Collection<String> openNodeIds) { if(!hasVisibleChildren(curRoot, tree)) { return false; } //open open nodes if(openNodeIds != null && !openNodeIds.isEmpty()) { if(openNodeIds.contains(curRoot.getIdent())) { return true; } else if (curRoot.getUserObject() instanceof String && openNodeIds.contains(curRoot.getUserObject())) { return true; } } //don't automatically open the children of the selected node if(selected && !tree.isExpandSelectedNode()) { return false; } //open the path of the selected node return (curSel == curRoot); } private boolean hasVisibleChildren(TreeNode curRoot, MenuTree tree) { boolean hasVisibleChild = false; if(curRoot.getChildCount() == 0) { //nothing to do } else if(tree.getFilter() != MenuTree.DEF_FILTER) { for(int i=curRoot.getChildCount(); i-->0 && !hasVisibleChild; ) { if(tree.getFilter().isVisible(curRoot.getChildAt(i))) { hasVisibleChild = true; } } } else { hasVisibleChild = true; } return hasVisibleChild; } private boolean isInsertionPointUnderNode(TreeNode curRoot, MenuTree tree) { return tree.getInsertionPoint() != null && tree.getInsertionPoint().getPosition() == Position.under && tree.getInsertionPoint().getNodeId().equals(curRoot.getIdent()); } }