HTML5 组件: 专有组件,第 2 部分

支持事件监听器,使用 CSS3 创建元素动画,并将内容注入到 DOM 中

在本 系列中,HTML5 专家 David Geary 将展示如何实现 HTML5 组件。在本期文章中,我们将完成 “专有组件,第 1 部分” 中介绍的复杂的专有 (ad-hoc) 滑块组件的实现。学习如何合并事件监听器,为滑块的手柄创建动画,并将滑块注入到现有的文档对象模型 (DOM) 树中。

David Geary, 作者和演讲家, Clarity Training, Inc.

David GearyDavid Geary 是 Core HTML5 Canvas 的作者,也是 HTML5 Denver User's Group 的合著者和其他 8 本 Java 图书的作者,其中包括畅销书 Swing 和 JavaServer Faces。David 经常在各大会议发表演讲,包括 JavaOne、Devoxx、Strange Loop、NDC 和 OSCON,他曾三次当选为 JavaOne 之星。他还为 developerWorks 撰写了 JSF 2 fuGWT fu 系列文章。您可以通过 @davidgeary 在 Twitter 上关注他。



2013 年 8 月 20 日

HTML5 是一种适用于 Web 的复杂编程平台,具有强大的功能,比如 canvas元素、CSS3 和一个事件模型。HTML5 提供了您实现自己的组件所需的所有基础功能。

关于本系列

HTML5 的标准 UI 组件模型仍在不断演化。在本 系列中,HTML5 专家 David Geary 将会展示如何使用现有的技术创建自己的专有 HTML5 组件,以及如何利用那些用于定义真实的 HTML5 组件系统的规范。

本文延续本系列的 第 1 期中关于滑块组件的讨论。您将学习如何合并事件监听器,如何创建滑块手柄的动画,以及如何将滑块本身注入现有的 DOM 树中。具体而言,您将学习如何:

  • 对滑块使用备用控件
  • 实现对注册和触发变更监听器的支持
  • 使用 CSS3 过渡为 HTML 元素创建动画
  • 将内容注入 DOM 树中
  • 使用元素属性自定义专有组件

备用滑块控件

图 1显示了来自 第 1 部分的使用滑块组件的简单应用程序的一个变体。该变体使用链接代替按钮,以递增和递减滑块的值。对于这些控件,可使用任何生成鼠标单击事件的元素。

图 1. 使用链接代替按钮
包含链接而不是按钮的滑块的屏幕截图

用户也可单击滑块的滑轨或拖动它的手柄来更改它的值。第 1 部分中的 “拖动手柄” 一节解释了滑块如何简化其拖动。在本文的 “为滑块手柄创建动画” 一节中,您将看到在用户单击滑块的滑轨时,滑块如何通过 CSS3 过渡来调整手柄的位置。

清单 1显示了 图 1中所示的应用程序的 HTML。

清单 1. 包含链接而不是按钮的滑块应用程序的 HTML
 <html> 
   <head> 
      ... 
   <head> 

   <body> 
      <div id='slider-component'> 
         <div id='controls'> 
            <a href='#'class='slider-control' id='decrement'>-</a> 
            <a href='#'class='slider-control' id='increment'>+</a> 

            <div id='slider'>  <!-- slider goes here-->  </div> 
         </div> 

         <div id='readout'>0</div> 
      </div> 
   </body> 

   <script type="text/javascript" src="lib/slider.js"></script> 
   <script type="text/javascript" src="sliderExample.js"></script> 
 </html>

清单 1中的 HTML 简单易懂。两个链接和两个滑块位于一个 controls DIV中,后者位于一个 slider-component DIV中。当用户单击某个链接时,应用程序的 JavaScript 就会处理单击事件,如 清单 2中所示。

清单 2. 链接单击事件
 var slider = new COREHTML5.Slider('black', 'cornflowerblue', 0), 
 ... 

 document.getElementById('decrement').onclick= function (e) { 
   slider.knobPercent -= 0.1; 
   slider.redraw(); 
   updateReadout(); 
 } 

 document.getElementById('increment').onclick= function (e) { 
   slider.knobPercent += 0.1; 
   slider.redraw(); 
   updateReadout(); 
 }

清单 2中的事件处理函数递增和递减滑块的值(存储在滑块的 knobPercent属性中),重新绘制滑块,并更新 readout元素。

滑块的值发生更改时,应用程序会使用附加到滑块的一个更改监听器来更新 readout元素,如 清单 3中所示。

清单 3. 滑块更改事件
 var readoutElement = document.getElementById('readout'); 

 function updateReadout() { 
   if (readoutElement) 
      readoutElement.innerHTML = slider.knobPercent.toFixed(2); 
 } 

 slider.addChangeListener(updateReadout);

请注意,readout 元素是可选的;如果该元素不存在,那么 updateReadout()方法不会执行任何操作。

下一节将会介绍滑块如何实现对更改监听器的支持。

图 1中所示的应用程序使用了滑块的 appendTo()方法,将滑块附加到 DOM:

 slider.appendTo('slider'); // Append the slider to the DOM element with an ID of slider 
 slider.draw();

本文将在 “将滑块附加到 DOM 元素” 一节中讨论该方法的实现。


支持滑块更改事件

上一节中,您已经了解了 图 1中的应用程序如何通过将变更监听器附加到滑块上,让可选的 readout 保持同步。清单 4展示了该滑块如何支持开发人员添加变更监听器,以及它如何为这些监听器触发变更事件。

清单 4. 添加变更监听器和触发事件
 COREHTML5.Slider = function { 
   ... 
   this.changeEventListeners= []; 
   ... 
 }; 

 COREHTML5.Slider.prototype = { 
   ... 

   addChangeListener: function (listenerFunction) { 
      this.changeEventListeners.push(listenerFunction); 
   }, 

   fireChangeEvent: function(e) { 
      for (var i=0; i < this.changeEventListeners.length; ++i) { 
         this.changeEventListeners[i](e); 
      } 
   }, 
   ... 
 };

每个滑块包含一个函数数组。当滑块的值发生更改时,fireChangeEvent()方法就会迭代该数组,依次调用每个函数。清单 5展示了滑块如何调用其 fireChangeEvent()方法。

清单 5. 触发更改事件
 COREHTML5.Slider.prototype = { 
   addMouseListeners: function () { 
      this.knobCanvas.addEventListener('mousemove', function(e) { 
         var mouse = null, 
             percent = null; 

         e.preventDefault(); 

         if (slider.draggingKnob) { 
            slider.deactivateKnobAnimation(); 
            
            mouse = slider.windowToCanvas(e.clientX, e.clientY); 
            percent = slider.knobPositionToPercent(mouse.x); 

            if (percent >= 0 && percent <= 1.0) { 
               slider.fireChangeEvent(e); 
               slider.erase(); 
               slider.draw(percent); 
            } 
         } 
      }, false); 

   },

用户拖动滑块的手柄时,一个附加到手柄画布的鼠标移动事件监听器将调整滑块的值,触发一个更改事件,然后擦除和重新绘制滑块。该事件监听器还在用户拖动手柄时取消激活滑块的手柄动画,以确保该动画不会妨碍用户对手柄的拖动。

现在您已知道滑块取消激活手柄动画的环境,让我们看看该动画的工作原理。


创建滑块手柄的动画

图 2展示了滑块如何实现其手柄的动画。图 2顶部的屏幕截图是在用户单击滑块的滑轨之前获取的。底部的两个屏幕截图显示了手柄朝光标位置的后续移动。(图 2非常近似实际动画;要获得完整的效果,请 下载代码并试用它。)

图 2. 创建滑块的动画
演示滑块动画的 3 个屏幕截图

如第 1 部分中的 “创建和初始化滑块” 一节中所述,滑块将它的滑轨和手柄绘制到一个独立的画布元素中。如果滑块在同一个画布中绘制滑块和手柄,那么操作将会更加简单;但 CSS3 过渡仅适用于单个元素,所以手柄必须位于独立的画布中。

上一节中,您已看到,在用户拖动手柄时,滑块调用了它的 deactivateKnobAnimation()方法来取消激活手柄动画。该方法和它的反向方法(activateKnobAnimation())如 清单 6中所示。

清单 6. 激活和取消激活手柄动画
 COREHTML5.Slider.prototype = { 
   ... 

   activateKnobAnimation: function () { 
      var transitionString= "margin-left" + 
          (this.knobAnimationDuration / 1000).toFixed(1) + "s"; 

      this.knobCanvas.style.webkitTransition= transitionString; 
      this.knobCanvas.style.MozTransition= transitionString; 
      this.knobCanvas.style.OTransition= transitionString; 
      this.knobCanvas.style.transition= transitionString; 
   }, 

   deactivateKnobAnimation: function () { 
      slider.knobCanvas.style.webkitTransition= "margin-left 0s"; 
      slider.knobCanvas.style.MozTransition= "margin-left 0s"; 
      slider.knobCanvas.style.OTransition= "margin-left 0s"; 
      slider.knobCanvas.style.transition= "margin-left 0s"; 
   }, 
   ... 
 };

浏览器之间的命名区别

像许多 HTML5 特性一样,CSS3 过渡最初是作为特定于浏览器的功能而实现的,然后才被 W3C 标准化。要确保该过渡适用于所有支持它们的浏览器版本,像 清单 6这样的代码必须容纳 CSS 属性名称的所有特定于浏览器的变体。

清单 6中所示的 activateKnobAnimation()方法以编程方式将一个 CSS3 过渡添加到手柄画布元素的 margin-leftCSS 属性中,并考虑到了过渡属性的名称本身的浏览器变体。该过渡的结果是,在更改 margin-left属性时,浏览器流畅地显示手柄画布从一个位置到另一个位置的动画。动画的持续时间使用滑块的 knobAnimationDuration属性指定,以毫秒为单位。

deactivateKnobAnimation()方法将 CSS3 过渡的持续事件更改为 0 秒,以便实际禁用动画。

当用户单击滑块的滑轨时,清单 7中所示的事件处理函数会将滑块的手柄移动到单击的位置。

清单 7. 单击滑块的滑轨
 COREHTML5.Slider.prototype = { 
   ... 
   addMouseListeners: function () { 
      ... 
   
      this.railCanvas.onmousedown= function(e) { 
         var mouse = slider.windowToCanvas(e.clientX, e.clientY), 
             startPercent, 
             endPercent; 

         e.preventDefault(); 

         startPercent = slider.knobPercent; 
         endPercent = slider.knobPositionToPercent(mouse.x); 

         slider.animatingKnob = true; 

         slider.moveKnob(mouse.x); 
         slider.trackKnobAnimation(startPercent, endPercent); 
      }; 
   }, 
   ... 
 };

滑块的 moveKnob()方法(如 清单 8中所示)通过设置手柄画布元素的 margin-leftCSS 属性来移动手柄。设置该属性可触发 CSS3 手柄动画,前提是该手柄动画已激活。

清单 8. 移动滑块的手柄
 COREHTML5.Slider.prototype = { 
   ... 

   moveKnob: function (position) { 
      this.knobCanvas.style.marginLeft= position - this.knobCanvas.width/2 + "px"; 
   }, 
   ... 
 };

JavaScript 中的 CSS 属性名称

许多 CSS 属性名称包含连字符,该符号不能在 JavaScript 属性名称中使用。这种不幸阻抗失配 (impedance mismatch) 需要您将包含连字符的 CSS 属性名称转换为驼峰式 JavaScript 属性名称;例如将 margin-left转换成 marginLeft,将 padding-top转换成 paddingTop

除了移动滑块的手柄,滑轨画布的鼠标按下事件处理函数调用滑块的 trackKnobAnimation()方法,如 清单 7中所示。该方法(将在下一节中讨论)在整个 CSS3 过渡的相应动画中保持滑块的值与手柄一致。


跟踪 CSS3 过渡

可使用一个事件监听器检测 CSS3 过渡的结束。为此,滑块组件从滑块构造函数中调用滑块的 addKnobTransitionListener()方法,如 清单 9所示。

清单 9. 添加手柄过渡监听器
 COREHTML5.Slider = function(strokeStyle, fillStyle, knobPercent, knobAnimationDuration) { 
   ... 
   this.createDOMTree(); 
   this.addMouseListeners(); 
   this.addKnobTransitionListener(); 
 };

滑块的 addKnobTransitionListener()方法(如 清单 10中所示)向手柄画布添加一个过渡监听器,再一次考虑浏览器之间的命名区别。

清单 10. CSS 过渡监听器
 COREHTML5.Slider.prototype = { 
   ... 

   addKnobTransitionListener: function () { 
      var BROWSER_PREFIXES= [ 'webkit', 'o' ]; 

      for (var i=0; i < BROWSER_PREFIXES.length; ++i) { 
         this.knobCanvas.addEventListener( 
            BROWSER_PREFIXES[0]+ "TransitionEnd", // Everything but Mozilla

            function (e) { 
               slider.animatingKnob = false; 
            } 
         ); 
      } 

      this.knobCanvas.addEventListener("transitionend", // Mozilla
         function (e) { 
            slider.animatingKnob = false; 
         } 
      ); 
   },      
   ...

手柄过渡动画完成时,浏览器会调用 清单 10中的过渡监听器。该监听器将滑块的 animatingKnob属性设置为 false,这导致滑块终止对手柄动画的跟踪。此跟踪功能由滑块的 trackKnobAnimation()方法实现,如 清单 11中所示。

清单 11. 跟踪手柄动画
trackKnobAnimation: function (startPercent, endPercent) { 
      var count = 0, 
          KNOB_ANIMATION_FRAME_RATE = 60,  // fps 
          iterations = slider.knobAnimationDuration/1000 * KNOB_ANIMATION_FRAME_RATE + 1, 
          interval; 

      interval = setInterval( function (e) { 
         if (slider.animatingKnob) {
            slider.knobPercent = startPercent + 
                                 ((endPercent - startPercent) / iterations * count++); 

            slider.knobPercent = slider.knobPercent > 1.0 ? 1.0 : slider.knobPercent; 
            slider.knobPercent = slider.knobPercent < 0 ? 0 : slider.knobPercent; 

            slider.fireChangeEvent(e); 
         } 
         else {// Done animating knob
            clearInterval(interval); 
            count = 0; 
         }
      }, slider.knobAnimationDuration / iterations); 
   }, 
   ...   
 };

尽管可通过事件监听器检测 CSS3 过渡动画的结束,如 清单 10中所示,但无法检测该动画的中间步骤,要在手柄动画播放期间触发更改事件,必须检测动画的中间步骤。

CSS3 过渡与动画

为了实现滑块手柄的动画,我使用了 CSS3 过渡,这相对容易使用,但无法在过渡的相应动画期间支持事件通知。我不能使用 CSS3 动画,它们更难使用,但它们在动画期间支持事件通知。

因为无法检测 CSS3 过渡动画的步骤,所以滑块的 trackKnobAnimation()方法使用 setInterval()来估算动画的步骤,并触发每一步骤中的更改事件。

现在您已了解如何使用 CSS3 过渡实现滑块的手柄动画,让我们看看如何将滑块附加到现有的 DOM 元素上。


将滑块附加到 DOM 元素

第 1 部分中的 “绘制滑块” 讨论了未引用 CSS3 过渡的滑块组件实现。该实现会得到滑块的一个 DOM 树,如 图 3中所示。

图 3. 滑块的 DOM 树
滑块的 DOM 树的插图,包含两个画布元素和一个 DIV

滑块的构造函数创建了一个 DIV和两个 canvas元素(分别用于滑块的滑轨和它的手柄),并将这些画布附加到 DIV。在将 清单 1中所示的应用程序架构滑块附加到现有的 DOM 元素时,滑块将它的闭包 DOM 元素附加到该现有元素,如 图 4中所示。

图 4. 合并滑块的 DOM 树
该图演示了滑块的 DOM 树的合并

滑块的构造函数调用了滑块的 createDOMTree()方法,如 清单 12中所示。

清单 12. 创建滑块的 DOM 树
 COREHTML5.Slider = function(strokeStyle, fillStyle, knobPercent, knobAnimationDuration) { 
   ... 
   this.createDOMTree(); 
   this.addMouseListeners(); 
   this.addKnobTransitionListener(); 
 };

清单 13显示了滑块的 createDOMTree()方法。

清单 13. createDOMTree()方法
 COREHTML5.Slider.prototype = { 
   ... 

   createDOMTree: function () { 
      var self = this; 

      this.domElement = document.createElement('div'); 

      this.domElement.appendChild(this.knobCanvas); 
      this.domElement.appendChild(this.railCanvas); 
   }, 
   ... 
 };

滑块调用 createDOMTree()时,它已经为手柄和滑轨创建了画布。createDOMTree()方法创建了滑块的闭包 DIV元素,然后将现有的手柄和滑轨添加到该元素。

滑块创建它的 DOM 树后,该阶段被设置为使用滑块的 appendTo()方法将滑块附加到现有的 DOM 元素上,如 清单 14中所示。

清单 14. appendTo()方法:将滑块附加到一个现有的 DOM 元素
 COREHTML5.Slider.prototype = { 
   ... 

   appendTo: function (elementName) { 
      if (typeof element === 'string') { 
         document.getElementById(element). 
            appendChild(this.domElement); 
      } 
      else { 
         element.appendChild(this.domElement); 
      } 

      this.resize(); 
   }, 
   ... 
 };

设置 CSS 大小和画布元素大小

清单 15中滑块的 setKnobCanvasSize()方法的最后 4 行证明了一个重要的事实:设置画布元素的 CSS 宽度和高度属性时,必须将画布元素的宽度和高度属性设置为相同值。这是因为 CSS 属性仅应用于画布元素,而画布宽度和高度属性会同时应用于画布元素和它的绘制表面。如果仅设置 CSS 属性,画布元素大小和它的绘制表面大小之间就会发生错误匹配。如 清单 15中所示,滑块的 resize()方法将会设置滑块的滑轨和手柄画布的大小,以及滑块本身的大小。

可向 appendTo()方法传递一个表示元素 ID 的字符串或元素本身;无论采用那种方式,该方法都会将滑块的闭包 DOM 元素附加到制定的元素,同时调整画布和滑块的闭包 DOM 元素。这个重新调整过程在 resize()方法和它调用的方法中执行,这些方法如 清单 15中所示。

清单 15. 调整滑块以适应其闭包元素
 COREHTML5.Slider.prototype = { 
   ... 

   setRailCanvasSize: function () { 
      var domElementParent = this.domElement.parentNode; 

      this.railCanvas.width = domElementParent.offsetWidth; 
      this.railCanvas.height = domElementParent.offsetHeight; 
   }, 

   
   setKnobCanvasSize: function () { 
      this.knobRadius = this.railCanvas.height/2 - 
                        this.railContext.lineWidth; 

      this.knobCanvas.style.width = this.knobRadius * 2 + "px"; 
      this.knobCanvas.style.height = this.knobRadius * 2 + "px"; 
      this.knobCanvas.width = this.knobRadius*2; 
      this.knobCanvas.height = this.knobRadius*2; 
   }, 

   setSliderSize: function() { 
      this.cornerRadius = (this.railCanvas.height/2 - 
                           2*this.VERTICAL_MARGIN)/2; 

      this.top = this.HORIZONTAL_MARGIN; 
      this.left = this.VERTICAL_MARGIN; 

      this.right = this.left + 
                   this.railCanvas.width - 2*this.HORIZONTAL_MARGIN; 

      this.bottom = this.top + 
                   this.railCanvas.height - 2*this.VERTICAL_MARGIN; 
   }, 

   resize: function() { 
      this.setRailCanvasSize(); 
      this.setKnobCanvasSize(); 
      this.setSliderSize(); 
   }, 
   ... 
 };

自定义组件

滑块组件(它目前已存在)很有用。但无论组件提供了多大的实用性,人们可能仍然希望能够以某种方式自定义该组件。这些自定义可能很简单(比如颜色更改),如 图 5所示,也可能很复杂(如支持垂直滑块,本文中讨论的滑块无法做到这一点)。

图 5. 一个水绿色滑块
一种新颜色(水绿色)滑块的屏幕截图

清单 16展示了如何将两种属性添加到滑块组件的 DIV元素,以处理滑块的 strokefill颜色。

清单 16. 指定属性元素
 <!DOCTYPE html> 
 <html> 
   <head> 
      ... 
   </head> 
   
   <body> 
      <div id='title'>A custom slider</div> 

      <p> 
         <div id='slider-component' stroke='blue' fill='aqua'> 
            ... 
         </div> 
      </p> 
   </body> 

   <script type="text/javascript" src="lib/slider.js"></script> 
   <script type="text/javascript" src="sliderExample.js"></script> 
 </html>

应用程序的 JavaScript 使用 getAttribute()方法(如 清单 17中所示)来获取 strokefill属性的值。

清单 17. 访问元素属性
 var sliderElement = document.getElementById('slider-component'), 
    slider = new COREHTML5.Slider(sliderElement.getAttribute('stroke'), 
                                  sliderElement.getAttribute('fill'), 
                                  0), 
    ...

随后,它将使用这些值创建滑块。


结束语

系列中的下一篇文章将讨论 W3C 的 “Web 组件简介” 规范,展示如何使用 Shadow DOM、自定义元素和模板来实现滑块组件。期待下次与您相见。


下载

描述名字大小
样例代码wa-html5-components-2-code.zip6KB

参考资料

学习

获得产品和技术

讨论

  • 加入 developerWorks 社区。探索由开发人员推动的博客、论坛、群组和维基,并与其他 developerWorks 用户进行交流。

条评论

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=Web development
ArticleID=941468
ArticleTitle=HTML5 组件: 专有组件,第 2 部分
publish-date=08202013