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

Key press handling in JSF component, especially <p:tree>

目标:我想用我自己的行为来丰富一个预定义的组件。这通常是列表、表格和树的情况,实现我的操作,如 "delete"、"add before"、"add after"、"move up"、...(对于文本字段,这似乎简单点...)

我认为必须有一种方法可以将关键侦听器附加到组件本身(假设有类似 "focus" 的东西),例如如果我在一个页面上有两棵树,按 "Ctrl+" 将一次通过 listenerA 添加一个 A 到 treeA,另一个通过 listenerB 添加一个 B 到 treeB。

在树节点或树本身添加 ajax 侦听器不起作用。因此,似乎有必要(请参阅下面的两个答案)在全局范围内捕获密钥并 "dispatch" 自己正确地捕获它们。至少对于一棵树,这应该可以毫不费力地工作。

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

由于我每年最多关心 2 次 JSF 问题,我认为更多参与的人可以深入了解 JSF 和 JavaScript 之间这个模糊地带的最佳实践。

在这段代码中,我想在按下“+”时创建一个新的子项目。

<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}" />

在"treeNode"和"tree"中不被接受并且在"form"中没有作用。

= 编辑

从答案中可以看出,只需使用 <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,最不丑陋的成语是什么。

= 编辑

我想指出一个简短的发现历史,作为下面的答案给出,以更详细地说明这个问题背后的问题

我找到了一个解决方法,它不完全符合要求,但可以处理我的情况。

向表单添加一个 "hotkey" 组件按要求调用服务器:

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

类似的组件存在于 RichFaces 中,不知道普通的 JSF。

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

并且像树或 table 这样的标准组件没有标准的键盘导航(那是 2015 年,我什至不记得 Web 2.0 是什么时候发明的)。

有什么最佳实践提示吗?

在更开明的大脑揭开秘密之前,还要进行更多调查...

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

<p:remoteCommand>

查看 Catch key pressed ajax event without input fields 了解丑陋的细节。

再次强调,这是一个全局关键问题,对组件不敏感。但很高兴知道。这也存在于纯 JSF 中吗?

此实现启用导航和 add/remove。

恕我直言,它具有最好的 functionality/effort 比率。

我不知道你用 标准 JSF 标签 普通 JSF 是什么意思,但在这个例子中 没有一行 JavaScript.

请注意 p:hotkey 组件行为是 全局的 p:tree 等非输入组件不能拥有密钥侦听器,因为它们不能是 "focused"(或者至少默认行为),就像您指出的那样。

但是,这里是:

<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. 请注意 p:tree 上的 pt:tabindex=1:需要启用 "focus"。 pt 是用于 "passthrough" 属性的名称空间,仅适用于 JSF 2.2。
  3. 我不得不自定义 HotkeyRenderer 以便将 DOM 事件侦听器附加到特定的 DOM 元素而不是整个文档:现在是 s:hotkey 而不是p:hotkey。我的实现将它附加到与父组件关联的 DOM 元素,继续阅读实现。

修改后的渲染器:

@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 定义(它是原始的 copy/paste,唯一的区别是 <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>

哇,好难 ;)

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

  • 一些 JSF 组件具有 "inner logic" 将一些键绑定到特定于组件的功能。太糟糕了,像 <p:tree /> 这样的 "intelligent components" 甚至不绑定箭头键导航。
  • 因此您尝试模拟并找到 <p:hotkey/>。不,你可以(如@michele-mariotti 的非常广泛的回答所示)对你的组件感到有点舒服。
  • 然后您将输入功能添加到树中...并且热键正在崩溃。您不知道出于什么原因(而且,实际上,我认为您不必...)。
  • 所以你开始四处挖掘,突然发现自己进入了 Java脚本和 DOM 仙境。
  • 无处不在的“hotkey”库jQuery似乎可以提供帮助。或者你在搜索这些东西时提到的 1000 个其他人中的一个。最好从一开始就选择正确的(是哪一个?)。
  • 因此您开始为每个加速器添加难看的 jQuery 表达式,首先是在文档上,然后是每个输入组件 (as shown e.g. here)。您的页面开始变得一团糟。
  • 但你很高兴 - 至少在两天后你已经长出了一棵简单的树..
  • 现在你加糖。您添加 <p:inplace /> 或简单地添加新的树节点。你的热键坏了。
  • 哦,是的,你应该知道:动态输入不绑定热键。向页面添加更多 Java脚本 hacks...
  • 但是,嘿,这是什么:测试所有热键内容,您忘记在树输入字段中输入值。现在你意识到:这是行不通的!!再次进行一些搜索:似乎是多年来众所周知的 bug/missing 功能。 Primefaces 在激活树输入后立即移除焦点。好吧,到底是谁在树中输入...
  • 因此,您可以在此处调试一些复杂的 Primefaces Java脚本或添加一些其他同样复杂的 Java脚本以强制将焦点返回到此字段。您可能会意识到您使用了错误的组件库,然后使用 Richfaces 树、Omnifaces 树或其他任何东西重新启动。你可以辞职去使用网络技术,再睡 2 年,然后回来看看基本技术是否已经发展到可以使用的地步。 Java 网络只是修补匠的游乐场吗?
  • 吐槽完这一段,有没有哪位大侠可以指点一下?