JSF 2 简介: HTML5 复合组件,第 2 部分

实现拖放

在本期 JSF 2 fu 中,系列作家 David Geary 继续演示 Java™Server Faces (JSF) 2 技术与 HTML5 结合的强大功能。在这一期中您将看到如何实现封装 HTML5 拖放功能的 JSF 复合组件。

David Geary, 总裁, Clarity Training, Inc.

David Geary 是一名作家、演讲家和顾问,也是 Clarity Training, Inc. 的总裁,他指导开发人员使用 JSF 和 Google Web Toolkit (GWT) 实现 Web 应用程序。他是 JSTL 1.0 和 JSF 1.0/2.0 专家组的成员,与人合作编写了 Sun 的 Web Developer 认证考试的内容,并且为多个开源项目作出贡献,包括 Apache Struts 和 Apache Shale。David 的 Graphic Java Swing 一直是关于 Java 的畅销书籍,而 Core JSF(与 Cay Horstman 合著)是关于 JSF 的畅销书。他还是 GWT Solutions 一书的作者。David 经常在各大会议和用户组发表演讲。他从 2003 年开始就一直是 NFJS tour 的定期演讲人,并且在 Java University 教授课程,三次当选为 JavaOne 之星。



2011 年 3 月 21 日

关于本系列

JSF 2 fu 系列是继 David Geary 同名的 三篇文章简介 推出的,像一个功夫大师一样帮助您发展和提升您的 JSF 2 框架技能。本系列深入研究框架及其周围的生态系统,并从外部视角展示一些 Java EE 技术,比如 Contexts and Dependency Injection 如何与 JSF 集成。

作为本文的基础前提,JSF 2 fu上一期 向您介绍了 HTML5 特性,展示了如何构建一个使 HTML5 画布易于使用的 JSF 组件。在本文中,我将简要介绍两个 JSF 复合组件的实现 — <h5:drag><h5:drop> — 利用 HTML5 基于事件的拖放机制(见 参考资料)。

拖放组件有以下 5 个重要特性:

  • 易于使用
  • 有条件拖拉
  • Ajax
  • 部分显示
  • 负载

从最基本的角度来看,<h5:drag><h5:drop> 都封装了一些变化莫测的 HTML5 拖放组件来增强易用性。例如,浏览器默认拒绝拖放,因此,在您的 drag-enter 和 drag-over 事件处理程序中,您必须取消浏览器的默认反应,通过删除事件来完成。这个非直观难点在 <h5:drag> 组件中处理,这使页面创建者可以将精力集中在更重要的事上。

<h5:drag><h5:drop> 组件支持有条件的拖拉(drag)。页面创建者可以基于传输中的数据和考虑中的拖放目标选择接受或拒绝放置(drop)。

运行样例代码

本系列的代码基于运行于企业容器(比如 GlassFish 或 Resin)内的 JSF 2。参阅本系列第一期 “JSF 2 fu: Ajax 组件”,这是个分步教程,讲解如何使用 GlassFish 安装和运行本系列的代码。在 下载 部分,获取本文的样例代码。

在 JSF 应用程序中,用户操作的数据大多数存储在服务器上,通常作为托管的 bean。由于这个原因,<h5:drop> 组件如果接受一个放置的话,将生成一个 Ajax 调用。页面创建者可以指定在 Ajax 调用返回时 JSF 将显示哪个组件。

<h5:drag><h5:drop> 组件也支持附加一些数据 — 通常称之为负载 — 到一个拖放指令中。页面创建者指定一个 bean 属性充当这个负载。在 Ajax 调用过程中,当出现放置时,由 <h5:drop> 组件启用,JSF 调用负载 bean 属性的 setter 方法。因此,JSF 像处理 <h:inputText> 元素值一样处理拖放负载。

使用拖拉源和放置目标

<h5:drag><h5:drop> 分别代表 HTML5 拖拉源和放置目标,在一个使用这些组件的 JSF 应用程序中,您可以像这样使用拖拉源:

<script>
   function dragStart(event) {
      event.dataTransfer.setData('text', "transfer this string");
   }
</script>

<h5:drag ondragstart="dragStart(event)">

   ...

</h5:drag>

页面创建者可以将一些组件或 HTML 元素放在 <h5:drag> 组件中,然后在 ondragstart 函数中设置传输数据,正如上面的标记。

您可以像这样使用放置目标:

<h5:drop id="dropzone" 
    payload="#{dragDrop.payload}" 
     render="@this"

   ...

</h5:drop>

与拖拉源一样,页面创建者可以将一些组件或 HTML 元素放在 <h5:drop> 组件中。页面创建者也可以指定一个 bean 属性作为拖放负载,且当在一个放置之后执行的 Ajax 调用从服务器返回时,指定 JSF 显示哪个组件。

页面创建者也可以选择使用 JavaScript 代码对拖放指令进行干预:

<h5:drop id="dropzone" 
    payload="#{dragDrop.payload}" 
     render="@this"
 ondragover="dragover(event)"
ondragenter="dragenter(event)" 
ondragleave="dragleave(event)"
     ondrop="drop(event)">

   ...

</h5:drop>

在上面的标记中,dragover()dragenter()dragleave()drop() 函数(没有显示)由页面创建者实现。

现在,您已经知道了本文的大体走向,接下来,我们讨论本文的拖放用例:Feeds 应用程序。


Feeds 应用程序

Feeds 应用程序(如图 1 所示)是一个 RSS 提要阅读器。左边菜单显示一个 RSS 提要列表,允许用户向列表中添加提要。页面中间显示当前提要的链接。当用户单击文章链接时,应用程序在浏览器中加载相关文章,用户随后可以单击 Back 按钮返回应用程序。

图 1. Feeds 应用程序
在浏览器中打开的 Feeds 应用程序的屏幕截图

一些给定 RSS 提要的文章列表定期更新,因此繁忙的用户可以保存链接供以后阅读,将文章链接从应用程序中间拉到右边菜单即可,如图 2 所示:

图 2. Feeds 应用程序中的拖放
Feeds 应用程序中的拖放

该应用程序有一个托管的 bean,如清单 1 所示,可以读取一个 RSS 提要并提供随后的 RSS 条目列表:

清单 1. 检索和解析 RSS 提要
package com.clarity;

import java.io.Serializable;

import java.net.URL;
import java.util.LinkedList;

import org.gnu.stealthp.rsslib.RSSChannel;
import org.gnu.stealthp.rsslib.RSSHandler;
import org.gnu.stealthp.rsslib.RSSItem;
import org.gnu.stealthp.rsslib.RSSParser;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@Named("rssFeed")@SessionScoped
public class RSSFeed implements Serializable {
   private static final long serialVersionUID = 2L;
   
   private String feed, displayName;
   private RSSChannel channel;
   private LinkedList<RSSItem> savedItems = new LinkedList<RSSItem>();

   public void fetch(String f, String dn) {
      assert f != null;
      assert dn != null;

      feed = f;
      displayName = dn;

      RSSHandler handler = new RSSHandler();
      channel = handler.getRSSChannel();

      try {
         RSSParser.parseXmlFile(new URL(feed), handler, true);
      } catch (Exception e) {
         channel = null;
         e.printStackTrace();
      }
   }

   public LinkedList<RSSItem> getItems() {
      return channel == null ? null : channel.getItems();
   }
   public LinkedList<RSSItem> getSavedItems() { 
      return savedItems;   
   }

   public RSSChannel getChannel() { return channel; }
   public String getFeed() { return feed; }
   public String getDisplayName() { return displayName; }
}

RSSFeed 类使用 RSSLib4J,这使得检索和解析一个 RSS 提要较为容易(见 参考资料)。由于有了 清单 1 中的 @Named@SessionScoped 属性,应用程序有一个会话范围内的托管的 bean,称为 rssFeed,是 RSSFeed 的一个实例。

应用程序左边菜单中的链接都可用于调用 rssFeed.fetch() 。例如,左边菜单中的 Apple 链接这样实现:

<h:commandLink value="Apple"
  action="#{rssFeed.fetch('http://rss.news.yahoo.com/rss/applecomputer', 
                          'Apple Computer')}"/>

当用户点击链接时,JSF 调用 rssFeed 托管的 bean 的 fetch() 方法,随后重新加载应用程序中间部分显示的链接列表:

<ui:repeat value="#{rssFeed.items}" var="item">
   <a href="#{item.link}">#{item.title}</a>
<ui:repeat>

该应用程序也显示在右边菜单中保存的链接:

<ui:repeat value="#{rssFeed.savedItems}" var="item">
   <a href="#{item.link}">
      #{ fn:substring(#{item.title}, 0, 25) } ...
   </a>
<ui:repeat>

现在您已经明白了 Feeds 应用程序如何从 RSS 提要中检索和显示条目,我会将注意力转移到本文的主要事件中。


<h5:drag> 和 <h5:drop> 组件

<h5:drag><h5:drop> 组件在 3 个文件中实现,每个组件各一个(drag.xhtml 和 drop.xhtml),另一个是 <h5:drop> 组件的 JavaScript 文件(drop.js),如图 3 所示:

图 3. <h5:drop><h5:drag> 组件的文件
拖放组件文件夹层次结构屏幕截图

尽管组件使得 Ajaxified 拖放非常容易,但其实现适中 。两个组件使用 facelets 标记和 JavaScript 完全实现:大约 100 行标记和 50 行 JavaScript。为了说明,我将在三个迭代中探究它们的开发:

  • 在客户端实现拖放
  • 发生放置时添加 Ajax 调用
  • 支持有条件拖拉

在客户端拖放

首先,我将精力集中在客户端。当用户在放置目标放置一个链接时,我只显示一个示警框,其中在文章的 URL 后显示标题。如图 4 所示:

图 4. 警示框显示在放置目标放置的链接
警示框显示在放置目标放置的链接

为了说明如何实现 图 4 所示的拖放,我将对以下工件做一一介绍:

  • 拖拉源
  • 拖拉源组件
  • 放置目标
  • 放置目标组件
  • 放置目标组件的 JavaScript

拖拉源

应用程序中间部分的每个链接(图 5 中高亮显示的)是一个拖拉源:

图 5. 拖拉源
拖拉源

清单 2 显示拖拉源的标记和 JavaScript:

清单 2. 拖拉源的标记和 JavaScript
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   xmlns:h5="http://java.sun.com/jsf/composite/html5"
   xmlns:places="http://java.sun.com/jsf/composite/places">

   <script>
      // event.target is one of the h5:drag elements generated by ui:repeat below
       function dragStart(event) {
          var linkref = event.target.firstElementChild.firstElementChild; // anchor
          var link = linkref.href;
          var title = linkref.textContent;

          event.dataTransfer.setData('text', title + " | " + link + " ");
      }
    </script>


   <h:panelGrid id="items" columns="1" >
     <ui:repeat value="#{rssFeed.items}" var="item">


       <h5:drag ondragstart="dragStart(event)">
        <p><a href="#{item.link}">#{item.title}</a> <br /></p>
      </h5:drag>

     </ui:repeat>
   </h:panelGrid>

</ui:composition>

清单 2 中的标记使用 <h5:drag> 组件,当一个拖拉启动时指定一个 JSF 调用的 JavaScript 函数。浏览器传递一个事件到此 JavaScript 函数。事件目标是 <h5:drag> 组件。该函数从 <h5:drag> 元素深入到 anchor 元素,获取链接的文本和引用。它将该信息嵌入到一个与 text 数据传输类型相关的字符串中。从那以后,当一个放置被接受时,HTML5 拖放系统将此字符串转换成放置目标。

拖拉源组件

<h5:drag> 组件的实现比较简单,如清单 3 所示:

清单 3. <h5:drag> 组件
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:composite="http://java.sun.com/jsf/composite">

   <composite:interface>
     <composite:attribute name="ondragstart"/>
   </composite:interface>
   
   <composite:implementation>
     <div draggable="true" 
         ondragstart="#{cc.attrs.ondragstart}">
        <composite:insertChildren />
     </div>   
   </composite:implementation>
</html>

<h5:drag> 组件创建一个可拖拉的 DIV,其 ondragstart JavaScript 是页面创建者在 清单 2 中指定的值。该组件也使用 <composite:insertChildren> 将任意标记插入 <h5:drag> 标签体中。在 清单 2 中,标记是链接的锚。

放置目标

如图 6 所示,放置目标在应用程序的右边菜单中:

图 6. 放置目标
放置目标

清单 4 显示了放置目标的实现:

清单 4. 放置目标
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:util="http://java.sun.com/jsf/composite/util"
   xmlns:fn="http://java.sun.com/jsp/jstl/functions"
   xmlns:h5="http://java.sun.com/jsf/composite/html5">

   <script>
        function drop(event) { alert(event.dataTransfer.getData("text")); }
    </script>

   <h5:drop id="dropzone"
      ondrop="drop(event)">

      <div class="welcomeImage">
          <h:graphicImage id="welcomeImage" 
             library="images" name="cloudy.gif"/>
      </div>

   </h5:drop>
</ui:composition>

放置目标由一个封装在 DIV 中的 <h5:drop> 组件构成,它含有云图片。<h5:drop> 组件的 ondrop 属性引用一个 JavaScript 函数,显示一个示警框,包含从拖拉源传输来的字符串。

放置目标组件

清单 5 显示 <h5:drop> 组件的实现:

清单 5. <h5:drop> 组件
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:composite="http://java.sun.com/jsf/composite">

   <composite:interface>
      <composite:attribute name="ondragenter"/>
      <composite:attribute name="ondragover"/>
      <composite:attribute name="ondragleave"/>
      <composite:attribute name="ondrop"/>
   </composite:interface>
   

   <composite:implementation>
      <div id="#{cc.id}" ondragenter="#{cc.attrs.ondragenter}"
                         ondrop="#{cc.attrs.ondrop}"
                         ondragover="#{cc.attrs.ondragover}"
                         ondragleave="#{cc.attrs.ondragleave}">
                         
        <composite:insertChildren />
          
      </div>
      
      <script> html5.jsf.init("#{cc.id}"); </script>
   </composite:implementation>
</html>

<h5:drop> 组件,像 <h5:drag> 一样,创建一个 DIV,然后将 <h5:drop> 标签体中的标记插入此 DIV 中。

当创建了 <h5:drop> 组件后,就可以调用 html5.jsf.init()。该函数通过添加 drag-enter 和 drag-over 事件处理程序初始化组件的 DIV。这些事件处理函数在清单 6 中实现:

清单 6. <h5:drop> component 的 JavaScript
if (!html5)
   var html5 = {}
if (!html5.jsf) {
   html5.jsf = {
      init : function(ccid) {
         var dropzone = $(ccid);

         dropzone.addEventListener("dragenter", function(event) {
            event.preventDefault();
         }, false);

         dropzone.addEventListener("dragover", function(event) {
            event.preventDefault();
         }, false);
      }
   };
}

清单 6(我使用名称空间来避免冲突)中的 JavaScript 阻止 浏览器对 drag-enter 和 drag-over 事件进行默认 响应。记住,默认情况下,浏览器取消那些事件,因此 清单 6 的 JavaScript 阻止 浏览器拒绝放置。可以说,进行那些 preventDefault() 调用是有点神秘,尽管如此,它是在可重用组件中进行封装的一个好的备用方案。

因为该 JavaScript 无条件阻止浏览器取消 drag-over 和 drag-enter 事件,它无条件接受所有 放置。在现实世界中是不可能的,因此我将在 有条件拖拉 中解决这一问题。接着,我将向您展示如何将 Ajax 添加到放置目标组件。


添加 Ajax 并发送一个负载到服务器

在客户端拖放 小节,我完全在客户端处理放置链接,通过呈现一个显示链接标题和 URL 的示警框(见 图 4)。对于 Feeds 应用程序以及大多数提供拖放的重要 JSF 应用程序而言,一个放置通常伴随着一次服务器访问,其中负载合并到服务器端数据。由于这一原因,在这一小节,我将 Ajax 添加到 <h5:drop> 组件,以便在放置发生时,该组件可以自动发起 Ajax 调用,并随 Ajax 调用发送负载。

每次当用户在右边菜单上放置一个链接时,<h5:drop> 组件发起一次 Ajax 调用,将链接添加到在服务器端保存的应用程序链接列表。当 Ajax 调用返回时,JSF 更新放置目标来反映最新添加的链接。

清单 7 显示更新的放置目标:

清单 7. 放置目标,Take II
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:fn="http://java.sun.com/jsp/jstl/functions"
   xmlns:h5="http://java.sun.com/jsf/composite/html5">

   <script>
        function dragenter(event) { /* Implement as desired */ }
        function dragleave(event) { /* Implement as desired */ }
        function dragover(event)  { /* Implement as desired */ }
        function drop(event)      { /* Implement as desired */ }
    </script>

   <h5:drop id="dropzone" payload="#{dragDrop.payload}" 
                           render="@this"
      ondragover="dragover(event)"
      ondragenter="dragenter(event)" 
      ondragleave="dragleave(event)"
      ondrop="drop(event)">

      <div class="welcomeImage">
          <h:graphicImage id="welcomeImage" 
             library="images" name="cloudy.gif"/>
      </div>
         
      <br />

      <div class="savedItems">
           <ui:repeat value="#{rssFeed.savedItems}" var="item">
             <div class="savedLink">
              <a href="#{item.link}">
                 #{ fn:substring(item.title, 0, 25) } ...
               </a>
               
               <br/>
           </div>
        </ui:repeat>      
      </div>

   </h5:drop>
</ui:composition>

在放置目标的这个版本中,我将拖放负载连接到一个 bean 属性:#{dragDrop.payload}。并且我通知放置目标显示 @this — 是指放置目标本身 — 当放置初始化的 Ajax 调用返回时。

清单 8 显示更新的 <h5:drop> 组件实现:

清单 8. <h5:drop> 组件,Take II
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:composite="http://java.sun.com/jsf/composite">

   <composite:interface>
      <composite:attribute name="ondragenter"/>
      <composite:attribute name="ondragover"/>
      <composite:attribute name="ondragleave"/>
      <composite:attribute name="ondrop"/>
      <composite:attribute name="render"/>
      <composite:attribute name="payload"/>
      <composite:attribute name="payloadType"/>
   </composite:interface>
   
   <composite:implementation>
      <h:outputScript library="javax.faces" name="jsf.js" target="head" />
      <h:outputScript library="html5" name="drop.js" target="head" />
        
      <div id="#{cc.id}" ondragenter="#{cc.attrs.ondragenter}"
                         ondrop="#{cc.attrs.ondrop}"
                         ondragover="#{cc.attrs.ondragover}"
                         ondragleave="#{cc.attrs.ondragleave}">
                         

        <composite:insertChildren />
          
        <h:form id="form">
          <h:inputText id="payload" 
                     value="#{cc.attrs.payload}" 
                     style="display: none"/>   
        </h:form>
          
        </div>
      
        <script> html5.jsf.init("#{cc.id}", 
                                "#{cc.attrs.payloadType}", 
                                "#{cc.attrs.render}"); </script>
   </composite:implementation>
</html>

更新的放置目标需要:

  • 当放置发生时,发起一个 Ajax 调用
  • 在 Ajax 调用过程中在服务器端提供负载

<h5:drop> 组件 JavaScript 中进行 Ajax 调用,如清单 9 所示,使用 JSF 的 jsf.ajax.request() JavaScript 函数:

清单 9. <h5:drop> 组件的 JavaScript,Take II
if (!html5)
   var html5 = {}
if (!html5.jsf) {
   html5.jsf = {
      init : function(ccid, payloadType, renderIds) {
         var dropzone = $(ccid);

         dropzone.payloadInput = $(ccid + ":form:payload");

         dropzone.addEventListener("drop", function(event) {
            if (payloadType == "")
               payloadType = "text";

            if (renderIds == "" || renderIds == "@this")
               renderIds = ccid;

            dropzone.payloadInput.value = event.dataTransfer
                  .getData(payloadType);jsf.ajax.request(dropzone.payloadInput, event, {
               render : renderIds,
               onevent : function(data) {
                   if (data.status == "success")
                      html5.jsf.init(ccid, payloadType, renderIds);
                }
            });
         }, false);

         dropzone.addEventListener("dragenter", function(event) {
            event.preventDefault();
         }, false);

         dropzone.addEventListener("dragover", function(event) {
            event.preventDefault();
         }, false);
      }
   };
}

重新调用 清单 7 中的负载名称:

<h5:drop...payload="#{dragDrop.payload}">

清单 8 中,我通过 <h5:drop> 组件的不可见输入文本 使负载可供使用。为了将负载传送到客户端,我需要一个在客户端和服务端来回传送数值的方法,而 JSF 已经在 <h:inputText> 中提供了。因此,我将一个不可见输入文本添加到放置目标,正如您在 清单 9 中所看到的那样,当生成一个放置时,在 Ajax 请求之前设置不可见输入的值。

因为我在发起 Ajax 调用之前设置了输入值,现在存储在输入值中的拖放负载在 Ajax 调用期间将在服务器端可用。因为输入值是连接到一个 bean 属性的,在 Ajax 调用期间 JSF 将传递负载到那个属性的 setter 方法中。

清单 8 中,该值被 <h5:drop> 组件使用,作为不可见文本输入的值:

<h:inputText id="payload" value="#{cc.attrs.payload}" style="display: none"/>

清单 9 中,不可见文本输入被指定为 Ajax 请求的源:

jsf.ajax.request(dropzone.payloadInput, event, {...});

由于不可见文本输入被指定为 Ajax 请求的源,JSF 在服务器端处理输入,这意味着它将调用与属性 setter 方法相关的文本输入 — 在本例中是 DragDrop.setPayload(),如清单 10 所示:

清单 10. payload 属性的实现
package com.clarity;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.StringTokenizer;

import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import javax.inject.Named;

import org.gnu.stealthp.rsslib.RSSItem;

@Named
@SessionScoped
public class DragDrop implements Serializable {
   @Inject private RSSFeed rssFeed;

   public DragDrop() {
   }
   
   public String getPayload() {
      // JSF requires both getters and setters for input properties
      return "";
   }
   
   public void setPayload(String payload) {
      // creates a new saved item, based on the payload. Payload 
      // was set in the drop event listener for the h5:drop component 
      // in /sections/feeds/menuLeft.xhtml
      StringTokenizer st = new StringTokenizer(payload);
      RSSItem item = new RSSItem();
      
      item.setTitle(st.nextToken("|"));
      st.nextToken(" ");
      item.setLink(st.nextToken(" "));
      
      rssFeed.getSavedItems().add(item);
   }
}

JSF 传递拖放负载到 DragDrop.setPayload()。基于该负载,DragDrop.setPayload() 方法创建一个新的 RSS 项,并将其添加到保存的 rssFeed 项清单中。注意,我还为负载属性包含一个 getter 方法,因为 JSF 需要 setter 和 getter 两个来处理输入值。

使用复合组件重用现有组件功能很简单 — 在本例中是 <h:inputText> 在服务器端与客户端传送数据的功能。我在 <h5:drop> 组件中使用不可见文本输入将数据从客户端传送到服务器端,只需要将文本输入添加到复合组件,然后在 Ajax 调用之前设置输入值。

现在我已经有了一个相对成熟的放置目标了(注意拖拉源从引入到现在没有改变),进行 Ajax 调用来响应放置,然后将转换的数据从客户端传递到服务器端。这也允许页面创建者在 Ajax 调用返回时指定他们想要显示的组件。但是我的拖放组件仍然缺乏一个特性:有条件拖拉。


有条件拖拉

清单 1 中,我将 Feeds 应用程序中的保存链接列表作为一个链表实现了,这意味着,用户可以添加同一文章的副本。我想要禁止这一行为,如图 7 所示。注意所托拉标题上的光标没有变成一个加号,就像 图 2 中的成功放置一样。

图 7. 禁用一个放置(光标不表示复制)
禁用一个放置(光标不表示复制)

为了禁用副本放置,我修改了 <h5:drop> 组件的 drag-enter 和 drag-over 事件处理程序来接受有条件放置,如清单 11 所示:

清单 11. <h5:drop> 组件的 JavaScript,Take III
if (!html5)
   var html5 = {}
if (!html5.jsf) {
   html5.jsf = {
      init : function(ccid, payloadType, renderIds) {
         var dropzone = $(ccid);


         if (dropzone.serverPayload) // already initialized
            return;
         
         dropzone.payloadInput = $(ccid + ":form:payload");
         dropzone.acceptDrop = false;
         dropzone.serverPayload = function() {
            return dropzone.payloadInput.value;
         };

         dropzone.addEventListener("drop", function(event) {
            if (payloadType == "")
               payloadType = "text";

            if (renderIds == "" || renderIds == "@this")
               renderIds = ccid;

            dropzone.payloadInput.value = event.dataTransfer
                  .getData(payloadType);
            
            jsf.ajax.request(dropzone.payloadInput, event, {
               render: renderIds
               onevent : function(data) {
                   if (data.status == "success")
                      html5.jsf.init(ccid, payloadType, renderIds);
                }
            });            
         }, false);

         dropzone.addEventListener("dragenter", function(event) {
            if (dropzone.acceptDrop)
               event.preventDefault();
         }, false);

         dropzone.addEventListener("dragover", function(event) {
            if (dropzone.acceptDrop)
               event.preventDefault();
         }, false);
      }
   };
   }

@Inject

注意 清单 12 中的 @Inject 注释。它提供对 rssFeed 托管的 bean 的访问。JSF 应用程序通常需要在 Java 代码中访问这些托管的 bean。您可以使用 JSF 和 JSP Expression Language API 来访问那些托管的 bean,或者仅使用 @Inject 注释。

添加 Ajax 并发送一个负载到服务器 小节,我主要关注将拖放负载从客户端传递到服务器端。为了接受有条件放置,我必须背道而驰:将信息从服务器端发送回客户端。在这种情况下,我需要访问保存链接列表,以便于我可以确定链接是否是一个副本。因此,为了将必要数据从服务器端传送回客户端,我添加了一个 serverPayload 变量到 dropzone

为了将保存链接列表发送回服务器,我将一个含有保存链接标题的字符串存储在放置目标的不可见文本输入中,通过 DragDrop.getPayload() 实现,如清单 12 所示:

清单 12. payload 属性的 getter 方法
package com.clarity;

// imports omitted. See 清单 10.

@Named
@SessionScoped
public class DragDrop implements Serializable {
   @Inject private RSSFeed rssFeed;

   public DragDrop() {
   }
   
   public String getPayload() {
      // sends a string that is a concatenation of the saved 
      // item's titles, back to the client
      LinkedList<RSSItem> savedItems = rssFeed.getSavedItems();
      Iterator<RSSItem> it = savedItems.iterator();
      String s = "";
      
      while (it.hasNext()) {
         RSSItem item = it.next();
         s += item.getTitle() + " | ";
      }
      return s;
   }
   
   public void setPayload(String payload) {
      // creates a new saved item, based on the payload. Payload 
      // was set in the drop event listener for the h5:drop component 
      // in /sections/feeds/menuLeft.xhtml
      StringTokenizer st = new StringTokenizer(payload);
      RSSItem item = new RSSItem();
      
      item.setTitle(st.nextToken("|"));
      st.nextToken(" ");
      item.setLink(st.nextToken(" "));
      
      rssFeed.getSavedItems().add(item);
   }
}

下一次初始化一个拖拉时,应用程序的拖拉源从服务器端检查负载(当然,这是不可见文本输入值) ,确任拖拉链接是否是一个副本,如清单 13 所示:

清单 13. 拖拉源,Take II
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   xmlns:h5="http://java.sun.com/jsf/composite/html5"
   xmlns:places="http://java.sun.com/jsf/composite/places">

   <script>
      // event.target is one of the h5:drag elements generated by ui:repeat below
       function dragStart(event) {
          var linkref = event.target.firstElementChild.firstElementChild; // anchor
          var link = linkref.href;
          var title = linkref.textContent;

          var dropzone = $("dropzone"); // h5:drop in dropZone.xhtml
          dropzone.acceptDrop = true;          
                    
          var serverPayload = dropzone.serverPayload();
          if (serverPayload.indexOf(title) != -1)
           dropzone.acceptDrop = false; // link already present
                       
          event.dataTransfer.setData('text', title + " | " + link + " ");
      }
    </script>

   <h:panelGrid columns="1" id="items">
     <ui:repeat value="#{rssFeed.items}" var="item">

       <h5:drag ondragstart="dragStart(event)">
        <p>
          <a href="#{item.link}">#{item.title}</a> <br />
        </p>
      </h5:drag>

      </ui:repeat>
   </h:panelGrid>

</ui:composition>

如果拖拉的链接是一个副本,拖拉源的 JavaScript 将放置目标的 acceptDrop 属性设置为 false。放置随后被放置源取消。


结束语

在本文中,我向您展示了如何使用封装了 HTML 拖放功能的 JFS 2 实现复合组件。复合组件是对 JSF 开发人员工具箱功能的增强,因为,这些组件使得您可以对组件实现重利用,而不需要编写 Java 代码或者执行任何配置。我也说明了如何重用现有组件,比如 JSF 的内置 <h:inputText> 元素,减少您在实现您自己的复合组件时的工作量。

当您开发了几个复合组件之后,有必要将它们组合在一个 JAR 文件中,这样其他开发人员就可以使用它们了。只需将它们打包成 JAR 文件,使用 META-INF 目录代替 WEB-INF。

在下一期 JSF2 fu 中,我将回顾我们在本系列中所介绍的内容,并推荐一些 JSF 2 的最佳实践。


下载

描述名字大小
本文样例代码j-jsf2fu-1110.zip5.39MB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=642599
ArticleTitle=JSF 2 简介: HTML5 复合组件,第 2 部分
publish-date=03212011