JSF组件中的按键处理,尤其是<p:tree> </p:tree>

时间:2015-01-06 21:39:57

标签: jsf-2 primefaces

目标:我想用自己的行为来丰富预定义的组件。这通常是列表,表格和树木的情况,执行我的操作,例如&#34;删除&#34;,&#34;添加之前&#34;,&#34;添加&#34;,&#34;移动up&#34;,...(文字字段似乎很简单......)

我认为必须有一种方法将关键听众附加到组件本身(假设有类似于&#34;焦点&#34;),例如如果我在页面上有两棵树,按下&#34; Ctrl +&#34;将通过listenerA将一次A添加到treeA,并通过listenerB将另一个B添加到treeB。

在树节点或树本身添加ajax侦听器不起作用。因此,似乎有必要(见下面的两个答案)抓住全球关键并且&#34;发送&#34;他们自己正确。至少有一棵树,这应该没有麻烦。

根据以下答案,这只能使用JavaScript或使用非标准JSF标记来完成。

由于我每年最多关注2次JSF问题,我认为有更多参与者可以在JSF和JavaScript之间的这个黄昏区域提供最佳实践的洞察力。

在这个片段中,我想在&#34; +&#34;时创建一个新的子项目。被压了。

<h:form>
    <p:tree id="document" value="#{demo.root}" var="node"
        selectionMode="single" selection="#{demo.selection}">
        <p:treeNode>
            <h:outputText value="#{node.label}" />
        </p:treeNode>
    </p:tree>
</h:form>

标签

<f:ajax event="keypress" listener="#{demo.doTest}" />
&#34; treeNode&#34>中不接受

;和&#34;树&#34;在&#34;形式&#34;。

中没有任何功能

=编辑

从答案中可以看出,只需使用<p:hotkey>即可支持此具体方案。这个解决方案有两个缺点,它的Primefaces绑定,如果我们添加像这样的输入组件

,它就会失败
<h:form>
    <p:tree id="document" value="#{demo.root}" var="node"
        selectionMode="single" selection="#{demo.selection}">
        <p:treeNode>
            <p:inputText value="#{node.label}" />
        </p:treeNode>
    </p:tree>
</h:form>

实施此类事情的最佳做法是什么?至少,在简单的JSF中有可能吗?如果我只使用普通的JSF,那么最难看的成语是什么。

=编辑

我想指出一个简短的调查结果历史,作为下面的答案给出,以提供有关这个问题背后问题的更多细节

4 个答案:

答案 0 :(得分:3)

此实现也可以启用导航和添加/删除。

恕我直言,它具有最佳的功能/努力比。

我不知道你对标准JSF标记普通JSF 的意思,但在这个例子中没有一行JavaScript < /强>

请注意,p:hotkey组件行为全局。像p:tree这样的非输入组件不能拥有密钥监听器,因为它们不能“聚焦”(或者至少是默认行为),就像你指出的那样。

但是,这是:

<h:form>
    <p:hotkey bind="left" actionListener="#{testBean.onLeft}" process="@form" update="target" />
    <p:hotkey bind="right" actionListener="#{testBean.onRight}" process="@form" update="target" />
    <p:hotkey bind="up" actionListener="#{testBean.onUp}" process="@form" update="target" />
    <p:hotkey bind="down" actionListener="#{testBean.onDown}" process="@form" update="target" />
    <p:hotkey bind="ctrl+a" actionListener="#{testBean.onAdd}" process="@form" update="target" />
    <p:hotkey bind="ctrl+d" actionListener="#{testBean.onDelete}" process="@form" update="target" />

    <h:panelGroup id="target">

        <p:tree value="#{testBean.root}" var="data" selectionMode="single"
            selection="#{testBean.selection}" dynamic="true">
            <p:treeNode expandedIcon="ui-icon-folder-open" collapsedIcon="ui-icon-folder-collapsed">
                <h:outputText value="#{data}" />
            </p:treeNode>
        </p:tree>

        <br />

        <h3>current selection: #{testBean.selection.data}</h3>

    </h:panelGroup>
</h:form>

这是托管bean:

@ManagedBean
@ViewScoped
public class TestBean implements Serializable
{
    private static final long serialVersionUID = 1L;

    private DefaultTreeNode root;

    private TreeNode selection;

    @PostConstruct
    public void init()
    {
        root = new DefaultTreeNode("node");
        root.setSelectable(false);

        DefaultTreeNode node_0 = new DefaultTreeNode("node_0");
        DefaultTreeNode node_1 = new DefaultTreeNode("node_1");
        DefaultTreeNode node_0_0 = new DefaultTreeNode("node_0_0");
        DefaultTreeNode node_0_1 = new DefaultTreeNode("node_0_1");
        DefaultTreeNode node_1_0 = new DefaultTreeNode("node_1_0");
        DefaultTreeNode node_1_1 = new DefaultTreeNode("node_1_1");

        node_0.setParent(root);
        root.getChildren().add(node_0);

        node_1.setParent(root);
        root.getChildren().add(node_1);

        node_0_0.setParent(node_0);
        node_0.getChildren().add(node_0_0);

        node_0_1.setParent(node_0);
        node_0.getChildren().add(node_0_1);

        node_1_0.setParent(node_1);
        node_1.getChildren().add(node_1_0);

        node_1_1.setParent(node_1);
        node_1.getChildren().add(node_1_1);

        selection = node_0;
        node_0.setSelected(true);
    }

    private void initSelection()
    {
        List<TreeNode> children = root.getChildren();
        if(!children.isEmpty())
        {
            selection = children.get(0);
            selection.setSelected(true);
        }
    }

    public void onLeft()
    {
        if(selection == null)
        {
            initSelection();
            return;
        }

        if(selection.isExpanded())
        {
            selection.setExpanded(false);
            return;
        }

        TreeNode parent = selection.getParent();
        if(parent != null && !parent.equals(root))
        {
            selection.setSelected(false);
            selection = parent;
            selection.setSelected(true);
        }
    }

    public void onRight()
    {
        if(selection == null)
        {
            initSelection();
            return;
        }

        if(selection.isLeaf())
        {
            return;
        }

        if(!selection.isExpanded())
        {
            selection.setExpanded(true);
            return;
        }

        List<TreeNode> children = selection.getChildren();
        if(!children.isEmpty())
        {
            selection.setSelected(false);
            selection = children.get(0);
            selection.setSelected(true);
        }
    }

    public void onUp()
    {
        if(selection == null)
        {
            initSelection();
            return;
        }

        TreeNode prev = findPrev(selection);
        if(prev != null)
        {
            selection.setSelected(false);
            selection = prev;
            selection.setSelected(true);
        }
    }

    public void onDown()
    {
        if(selection == null)
        {
            initSelection();
            return;
        }

        if(selection.isExpanded())
        {
            List<TreeNode> children = selection.getChildren();
            if(!children.isEmpty())
            {
                selection.setSelected(false);
                selection = children.get(0);
                selection.setSelected(true);
                return;
            }
        }

        TreeNode next = findNext(selection);
        if(next != null)
        {
            selection.setSelected(false);
            selection = next;
            selection.setSelected(true);
        }
    }

    public void onAdd()
    {
        if(selection == null)
        {
            selection = root;
        }

        TreeNode node = createNode();
        node.setParent(selection);
        selection.getChildren().add(node);
        selection.setExpanded(true);

        selection.setSelected(false);
        selection = node;
        selection.setSelected(true);
    }

    public void onDelete()
    {
        if(selection == null)
        {
            return;
        }

        TreeNode parent = selection.getParent();
        parent.getChildren().remove(selection);

        if(!parent.equals(root))
        {
            selection = parent;
            selection.setSelected(true);

            if(selection.isLeaf())
            {
                selection.setExpanded(false);
            }
        }
        else
        {
            selection = null;
        }

    }

    // create the new node the way you like, this is an example
    private TreeNode createNode()
    {
        int prog = 0;
        TreeNode lastNode = Iterables.getLast(selection.getChildren(), null);
        if(lastNode != null)
        {
            prog = NumberUtils.toInt(StringUtils.substringAfterLast(String.valueOf(lastNode.getData()), "_"), -1) + 1;
        }

        return new DefaultTreeNode(selection.getData() + "_" + prog);
    }

    private TreeNode findNext(TreeNode node)
    {
        TreeNode parent = node.getParent();
        if(parent == null)
        {
            return null;
        }

        List<TreeNode> brothers = parent.getChildren();
        int index = brothers.indexOf(node);
        if(index < brothers.size() - 1)
        {
            return brothers.get(index + 1);
        }

        return findNext(parent);
    }

    private TreeNode findPrev(TreeNode node)
    {
        TreeNode parent = node.getParent();
        if(parent == null)
        {
            return null;
        }

        List<TreeNode> brothers = parent.getChildren();
        int index = brothers.indexOf(node);
        if(index > 0)
        {
            return findLastUnexpanded(brothers.get(index - 1));
        }

        if(!parent.equals(root))
        {
            return parent;
        }

        return null;

    }

    private TreeNode findLastUnexpanded(TreeNode node)
    {
        if(!node.isExpanded())
        {
            return node;
        }

        List<TreeNode> children = node.getChildren();
        if(children.isEmpty())
        {
            return node;
        }

        return findLastUnexpanded(Iterables.getLast(children));
    }

    public TreeNode getRoot()
    {
        return root;
    }

    public TreeNode getSelection()
    {
        return selection;
    }

    public void setSelection(TreeNode selection)
    {
        this.selection = selection;
    }
}

<强>更新

也许我找到了一个有趣的解决方案来将键绑定附加到单个DOM元素:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:cc="http://xmlns.jcp.org/jsf/composite" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
    xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions" xmlns:p="http://primefaces.org/ui"
    xmlns:o="http://omnifaces.org/ui" xmlns:of="http://omnifaces.org/functions"
    xmlns:s="http://shapeitalia.com/jsf2" xmlns:sc="http://xmlns.jcp.org/jsf/composite/shape"
    xmlns:e="http://java.sun.com/jsf/composite/cc" xmlns:pt="http://xmlns.jcp.org/jsf/passthrough">

<h:head>
    <title>test hotkey</title>
</h:head>

<h:body>
    <h:form>
        <h:panelGroup id="container1">
            <s:hotkey bind="left" actionListener="#{testBean.onLeft}" update="container1" />
            <s:hotkey bind="right" actionListener="#{testBean.onRight}" update="container1" />
            <s:hotkey bind="up" actionListener="#{testBean.onUp}" update="container1" />
            <s:hotkey bind="down" actionListener="#{testBean.onDown}" update="container1" />
            <s:hotkey bind="ctrl+a" actionListener="#{testBean.onAdd}" update="container1" />
            <s:hotkey bind="ctrl+d" actionListener="#{testBean.onDelete}" update="container1" />

            <p:tree value="#{testBean.root}" var="data" selectionMode="single"
                selection="#{testBean.selection}" dynamic="true" pt:tabindex="1">
                <p:treeNode expandedIcon="ui-icon-folder-open"
                    collapsedIcon="ui-icon-folder-collapsed">
                    <h:outputText value="#{data}" />
                </p:treeNode>
            </p:tree>
            <br />

            <h3>current selection: #{testBean.selection.data}</h3>
        </h:panelGroup>
    </h:form>
</h:body>
</html>

三件重要的事情:

  1. h:panelGroup属性id是必需的,否则它不会呈现为DOM元素。 stylestyleClass和其他渲染使能属性可以与之一起使用。
  2. 请注意pt:tabindex=1上的p:tree:需要启用“焦点”。 pt是用于“passthrough”属性的命名空间,仅适用于JSF 2.2。
  3. 我必须自定义HotkeyRenderer才能将DOM事件监听器附加到特定的DOM元素而不是整个文档:现在它是s:hotkey而不是p:hotkey。我的实现将它附加到与父组件关联的DOM元素,继续读取以实现。
  4. 修改后的渲染器:

    @FacesRenderer(componentFamily = Hotkey.COMPONENT_FAMILY, rendererType = "it.shape.HotkeyRenderer")
    public class HotkeyRenderer extends org.primefaces.component.hotkey.HotkeyRenderer
    {
        @SuppressWarnings("resource")
        @Override
        public void encodeEnd(FacesContext context, UIComponent component) throws IOException
        {
            ResponseWriter writer = context.getResponseWriter();
            Hotkey hotkey = (Hotkey) component;
            String clientId = hotkey.getClientId(context);
    
            String targetClientId = hotkey.getParent().getClientId();
    
            writer.startElement("script", null);
            writer.writeAttribute("type", "text/javascript", null);
    
            writer.write("$(function() {");
            writer.write("$(PrimeFaces.escapeClientId('" + targetClientId + "')).bind('keydown', '" + hotkey.getBind() + "', function(){");
    
            if(hotkey.isAjaxified())
            {
                UIComponent form = ComponentUtils.findParentForm(context, hotkey);
    
                if(form == null)
                {
                    throw new FacesException("Hotkey '" + clientId + "' needs to be enclosed in a form when ajax mode is enabled");
                }
    
                AjaxRequestBuilder builder = RequestContext.getCurrentInstance().getAjaxRequestBuilder();
    
                String request = builder.init()
                    .source(clientId)
                    .form(form.getClientId(context))
                    .process(component, hotkey.getProcess())
                    .update(component, hotkey.getUpdate())
                    .async(hotkey.isAsync())
                    .global(hotkey.isGlobal())
                    .delay(hotkey.getDelay())
                    .timeout(hotkey.getTimeout())
                    .partialSubmit(hotkey.isPartialSubmit(), hotkey.isPartialSubmitSet())
                    .resetValues(hotkey.isResetValues(), hotkey.isResetValuesSet())
                    .ignoreAutoUpdate(hotkey.isIgnoreAutoUpdate())
                    .onstart(hotkey.getOnstart())
                    .onerror(hotkey.getOnerror())
                    .onsuccess(hotkey.getOnsuccess())
                    .oncomplete(hotkey.getOncomplete())
                    .params(hotkey)
                    .build();
    
                writer.write(request);
    
            }
            else
            {
                writer.write(hotkey.getHandler());
            }
    
            writer.write(";return false;});});");
    
            writer.endElement("script");
        }
    }
    

    最后这是新s:hotkey的taglib定义(它是原始文件的复制/粘贴,唯一区别为<renderer-type>it.shape.HotkeyRenderer</renderer-type>):

    <?xml version="1.0" encoding="UTF-8"?>
    <facelet-taglib version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facelettaglibrary_2_2.xsd">
        <namespace>http://shapeitalia.com/jsf2</namespace>
    
        <tag>
            <description><![CDATA[HotKey is a generic key binding component that can bind any formation of keys to javascript event handlers or ajax calls.]]></description>
            <tag-name>hotkey</tag-name>
            <component>
                <component-type>org.primefaces.component.Hotkey</component-type>
                <renderer-type>it.shape.HotkeyRenderer</renderer-type>
            </component>
            <attribute>
                <description><![CDATA[Unique identifier of the component in a namingContainer.]]></description>
                <name>id</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Boolean value to specify the rendering of the component, when set to false component will not be rendered.]]></description>
                <name>rendered</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
            <attribute>
                <description><![CDATA[An el expression referring to a server side UIComponent instance in a backing bean.]]></description>
                <name>binding</name>
                <required>false</required>
                <type>javax.faces.component.UIComponent</type>
            </attribute>
            <attribute>
                <description><![CDATA[An actionlistener that'd be processed in the partial request caused by uiajax.]]></description>
                <name>actionListener</name>
                <required>false</required>
                <type>javax.faces.event.ActionListener</type>
            </attribute>
            <attribute>
                <description><![CDATA[A method expression that'd be processed in the partial request caused by uiajax.]]></description>
                <name>action</name>
                <required>false</required>
                <type>javax.el.MethodExpression</type>
            </attribute>
            <attribute>
                <description><![CDATA[Boolean value that determines the phaseId, when true actions are processed at apply_request_values, when false at invoke_application phase.]]></description>
                <name>immediate</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
            <attribute>
                <description><![CDATA[The Key binding. Required.]]></description>
                <name>bind</name>
                <required>true</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Client side id of the component(s) to be updated after async partial submit request.]]></description>
                <name>update</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Component id(s) to process partially instead of whole view.]]></description>
                <name>process</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Javascript event handler to be executed when the key binding is pressed.]]></description>
                <name>handler</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Javascript handler to execute before ajax request is begins.]]></description>
                <name>onstart</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Javascript handler to execute when ajax request is completed.]]></description>
                <name>oncomplete</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Javascript handler to execute when ajax request fails.]]></description>
                <name>onerror</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Javascript handler to execute when ajax request succeeds.]]></description>
                <name>onsuccess</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Global ajax requests are listened by ajaxStatus component, setting global to false will not trigger ajaxStatus. Default is true.]]></description>
                <name>global</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
            <attribute>
                <description><![CDATA[If less than delay milliseconds elapses between calls to request() only the most recent one is sent and all other requests are discarded. The default value of this option is null. If the value of delay is the literal string 'none' without the quotes or the default, no delay is used.]]></description>
                <name>delay</name>
                <required>false</required>
                <type>java.lang.String</type>
            </attribute>
            <attribute>
                <description><![CDATA[Defines the timeout for the ajax request.]]></description>
                <name>timeout</name>
                <required>false</required>
                <type>java.lang.Integer</type>
            </attribute>
            <attribute>
                <description><![CDATA[When set to true, ajax requests are not queued. Default is false.]]></description>
                <name>async</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
            <attribute>
                <description><![CDATA[When enabled, only values related to partially processed components would be serialized for ajax 
                instead of whole form.]]></description>
                <name>partialSubmit</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
            <attribute>
                <description><![CDATA[If true, indicate that this particular Ajax transaction is a value reset transaction. This will cause resetValue() to be called on any EditableValueHolder instances encountered as a result of this ajax transaction. If not specified, or the value is false, no such indication is made.]]></description>
                <name>resetValues</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
            <attribute>
                <description><![CDATA[If true, components which autoUpdate="true" will not be updated for this request. If not specified, or the value is false, no such indication is made.]]></description>
                <name>ignoreAutoUpdate</name>
                <required>false</required>
                <type>java.lang.Boolean</type>
            </attribute>
        </tag>
    </facelet-taglib>
    

    哇,这很难;)

答案 1 :(得分:1)

到目前为止,到目前为止还没有真正令人满意的答案。我总结了我的发现:

  • 一些JSF组件具有内部逻辑&#34;将某些键绑定到组件特定功能。太糟糕了#34;智能组件&#34;比如<p:tree />甚至不绑定箭头键导航。
  • 因此,您尝试模拟并找到<p:hotkey/>。不,你可以(如@ michele-mariotti的广泛答案所示)感觉你的组件有点舒服。
  • 然后你将输入功能添加到树中...而且热键正在崩溃。你不知道是什么原因(而且,实际上,我认为你不应该......)。
  • 所以你开始挖掘并突然发现自己处于JavaScript和DOM仙境中。
  • &#34; hotkey&#34;无处不在的jQuery库似乎带来了帮助。或者在搜索这些东西时你带来的其他1000个人中的一个。最好从一开始就采取正确的方法(哪一个呢?)。
  • 因此,您开始为每个加速器添加丑陋的jQuery表达式,首先是在文档上,然后是每个输入组件(as shown e.g. here)。你的网页开始变得一团糟。
  • 但你很高兴 - 至少在两天之后你养了一棵简单的树..
  • 现在加糖。您添加<p:inplace />或只是添加新的树节点。你的热键坏了。
  • 哦,是的,你应该知道:动态输入没有绑定到热键。在页面中添加更多JavaScript黑客......
  • 但是,嘿,这是什么:测试你所有的热键内容,你忘了在树输入字段中输入值。现在你意识到:它不起作用!!再一些搜索:多年来似乎是一个众所周知的错误/缺失功能。 Primefaces在激活树输入后立即移除焦点。好吧,谁在地球上输入一个树......
  • 所以,在这里你可以调试一些复杂的Primefaces JavaScript或添加一些其他同样复杂的JavaScript来强制重点回到这个领域。您可能会意识到您使用了错误的组件库,并使用Richfaces树,Omnifaces树或其他任何东西重新启动。您可以辞职使用网络技术,再睡2年,然后回来看看基本技术是否已经发展为可用。 Java web只是修补匠的游乐场吗?
  • 在这片咆哮之后,是否有人可以提供一些建议?

答案 2 :(得分:0)

我发现了一种与确切问题无关的解决方法,但可以处理我的方案。

向表单添加“热键”组件会根据请求调用服务器:

<p:hotkey bind="ctrl+shift+a" update="messages" actionListener="#{demo.doTest}"/>

RichFaces中存在类似的组件,不了解普通的JSF。

我无法相信的是,没有其他方法可以回归到JavaScript(如http://winechess.blogspot.ru/2014/02/datatable-keyboard-navigation.htmlhttp://www.openjs.com/scripts/events/keyboard_shortcuts/)来编写可用的JSF应用程序?

树或表等标准组件没有标准的键盘导航(2015年,我甚至不记得发布Web 2.0的时候)。

任何最佳做法提示?

答案 3 :(得分:0)

在更开明的大脑之前进行更多调查可以解除秘密......

有点类似的q / a解决了如果在JS中处理密钥时如何从JS调用后端方法的问题 - 使用

<p:remoteCommand>

请参阅Catch key pressed ajax event without input fields了解丑陋的细节。

同样,这是一个全局关键问题,而不是组件敏感问题。但很高兴知道。这是否也存在于普通的JSF中?