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



>目标:我想用自己的行为来丰富预定义的组件。列表、表和树通常就是这种情况,实现我的操作,如"删除"、"添加之前"、"添加后"、"上移,...(使用文本字段,这似乎很简单...

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

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

根据下面的答案,这只能使用 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,那将是最不丑陋的成语。

= 编辑

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

此实现还支持导航和添加/删除。

恕我直言,它具有最佳的功能/工作量比。

我不知道标准 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 是用于"直通"属性的命名空间,仅适用于 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 定义(它是原始的复制/粘贴,唯一的区别是 <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 组件具有"内部逻辑",可将某些键绑定到特定于组件的功能部件。太糟糕了,像<p:tree />这样的"智能组件"甚至不绑定箭头键导航。
  • 所以你试着模仿并找到<p:hotkey/>.不,你可以(如@michele-mariotti非常广泛的答案所示)在你的组件上感觉有点舒服。
  • 然后,将输入要素添加到树中...热键正在崩溃。你不知道出于什么原因(而且,真的,我认为你不应该......
  • 所以你开始四处挖掘,突然发现自己置身于 JavaScript 和 DOM 仙境。
  • 无处不在的jQuery的"热键"库似乎带来了帮助。或者您在搜索这些东西时提出的其他 1000 个中的一个。最好从一开始就选择正确的(是哪一个?
  • 因此,您开始为每个加速器添加丑陋的jQuery表达式,首先在文档上,然后在每个输入组件上(如下所示)。您的页面开始一团糟。
  • 但你很开心——至少两天后你养了一棵简单的树。
  • 现在你加糖。您可以添加<p:inplace />或只是添加新的树节点。您的热键坏了。
  • 哦,是的,您应该知道:动态输入不绑定到热键。向页面添加更多JavaScript黑客...
  • 但是,嘿,这是什么:测试所有热键内容时,您忘记在树输入字段中输入值。现在你意识到:它不起作用!!再次进行一些搜索:多年来似乎是一个众所周知的错误/缺失功能。Primefaces在激活树输入后立即删除焦点。好吧,到底是谁在树上输入...
  • 所以,在这里你可以调试一些复杂的Primefaces JavaScript,或者添加一些其他同样复杂的JavaScript,以迫使焦点回到这个领域。您可能会意识到您使用了错误的组件库,并使用Richfaces树,Omnifaces树或其他任何东西重新启动。你可以辞职使用网络技术,再睡2年,然后回来看看基本技术是否已经发展到可用。Java Web只是修补匠的游乐场吗?
  • 在这篇咆哮之后,有没有人可以帮助提供一些建议?

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

将"热键"组件添加到表单中会根据请求调用服务器:

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

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

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

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

有什么最佳实践提示吗?

更开明的大脑可以解开秘密之前,还需要进行更多的调查......

一个有点类似的问答解决了如果在JS中处理密钥,如何从JS调用后端方法的问题 - 使用"

<p:remoteCommand>

有关丑陋的详细信息,请参阅捕获没有输入字段的按键按下的 ajax 事件。

同样,这是一个全局键捕获,不区分组件。但很高兴知道。这在普通 JSF 中也存在吗?

最新更新