【问题标题】:Key press handling in JSF component, especially <p:tree>JSF 组件中的按键处理,尤其是 <p:tree>
【发布时间】:2015-01-06 21:39:57
【问题描述】:

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

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

在树节点或树本身添加 ajax 侦听器不起作用。因此,似乎有必要(见下面的两个答案)在全局范围内捕获密钥并自己正确“调度”它们。至少对于一棵树,这应该很容易。

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

由于我每年最多关注 2 次 JSF 问题,我认为更多参与的人可以就 JSF 和 JavaScript 之间的这个黄昏地带的最佳实践提供见解。

在这个 sn-p 中,我想在按下“+”时创建一个新的子项。

<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”中没有功能。

= 编辑

从答案中可以看出,只需使用&lt;p:hotkey&gt; 即可支持此具体场景。这个解决方案有两个缺点,它的 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,什么是最不丑陋的成语。

= 编辑

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

【问题讨论】:

  • 您能否澄清问题本身并保留具体问题?您发布了两个答案,其中您似乎发布了更多(相关)问题。但目前还不清楚最终实际询问的是什么。
  • @BalusC - 重述问题 - 现在这更有意义了吗?

标签: jsf-2 primefaces


【解决方案1】:

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

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

我不知道您对 standard JSF tagplain 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=1p: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 定义(它是原版的复制/粘贴,唯一的区别是 &lt;renderer-type&gt;it.shape.HotkeyRenderer&lt;/renderer-type&gt;):

<?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 中处理加速器的方式”之类的答案。
  • 查看更新的答案。也许这不是处理加速器的最终方法,但它是向前迈出的一步;)
  • 感谢您在这个主题上的巨额投资。你准备好运行 tree sn-p,我已经收获了很多。通过对组件内部结构的详细剖析,我肯定会学到很多东西。希望你能在美好的一天坚持我的另一个问题......顺便说一句。 “pt:tabindex=1 on p:tree: it is required to enable "focus"”是什么意思???
  • 不客气,这对我来说也是一个很好的游乐场。这里有一点 fiddle 来显示 tabindex 的东西:点击索引的 div 并按下一个键。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    我找到了一种解决方法,它与所要求的不完全一样,但可以处理我的情况。

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

    <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 是什么时候发明的)。

    任何最佳实践提示?

    【讨论】:

      【解决方案4】:

      在更开明的大脑解开这个秘密之前,还要进行一些调查......

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

      <p:remoteCommand>
      

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

      同样,这是一个全局键捕获,不是组件敏感的。但很高兴知道。这在纯 JSF 中也存在吗?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-10-05
        • 1970-01-01
        • 1970-01-01
        • 2011-07-05
        • 2012-02-23
        • 2011-12-14
        • 2012-05-27
        • 2013-02-28
        相关资源
        最近更新 更多