此实现还支持导航和添加/删除。
恕我直言,它具有最佳的功能/工作量比。
我不知道您对 standard JSF tag 或 plain 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>
三个重要的事情:
-
h:panelGroup 属性 id 是必需的,否则不会呈现为 DOM 元素。 style、styleClass 和其他启用渲染的属性可以与或代替使用。
- 注意
pt:tabindex=1对p:tree:需要启用“焦点”。 pt 是用于“直通”属性的命名空间,仅适用于 JSF 2.2。
- 我必须自定义
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>
哇,太难了;)