zoukankan      html  css  js  c++  java
  • ZLComboBox自定义控件开发详解

    【引言】距离上一回写博客已经有一些时日了,之前的爱莲iLinkIT系列主要是讲解了如何用NodeJS来实现一个简单的“文件传送”软件,属于JavaScript中在服务器端的应用。

    今天,我们就回归到JavaScript的主战场 ---- 前端设计,一起来聊聊如何开发一个“自定义”的Web控件(基于jQuery),属于基础级别,高手请轻拍,还望不吝赐教,先谢过。

    我们先假设这样一个场景,BOSS下达了命令,要求开发一款“幼儿早教”的应用,方便小朋友进行选择操作,而你则负责设计一个“下拉框”的Web控件,方便团队中其他弟兄使用,设计师的MM已经把效果图设计好了,如下:

    总的来说,这是一个类似“下拉框”的Web控件:

    a. 用户单击控件之后,显示下拉列表。

    b. 选中下拉列表中的某项(例如:桃子),会将选中项的图片显示出来。

    接下来,就让我们一步一步来实现这样的功能,我们先对整个讲解过程做一个整体的规划,以便我们心中有谱:

    1. 实现控件的外观部分:包括HTML代码的组织,CSS样式设计。

    2. 实现控件的基本框架:用JavaScript实现控件的交互设计。

    3. 优化控件的可扩展性:让控件可以接受JavaScript回调函数。

    4. 优化控件的可用性:可以动态传入选项的数据,达到自定义选项列表的目的。

    最后,我们来做一个简单的回顾和总结。单击此处下载演示包,解压缩后打开index.html页面体验。

    实现控件的外观部分

    我们先来分析一下整个控件的几个组成部分,然后组织我们的HTML代码,为了后面设计CSS样式表的方便,在组织HTML代码时,就应该遵照相关规范(例如:公司的样式表命名规范),完成class的名称设计。

    控件的组成

    控件的最外层是一个大的容器,包含控件的各个组成部分,其中的组件包括:
    1. 结果显示框

    可以将选中的选项显示在结果显示框中。

    2. 下拉框按钮

    控制下拉框的显示与隐藏,单击按钮,显示下拉列表,再次单击时,隐藏下拉框,不同的状态下,箭头的方向不一样。

    3. 下拉框

    下拉框中包含多个可供用户选择的“项”。

    4. 下拉框中的项

    选项包含图片和文字两个部分,当前被选中的选项,应该呈现不同的外观。 

    HTML代码

    根据前面的组成分析,我们可以实现如下的HTML代码(index.html):

     1 <div class="zl_combobox">
     2 <div class="cb_input"><img src="images/f_apple.png" /><span>苹果</span></div>
     3 <span class="cb_btn"></span>
     4         <ul class="item_list">
     5                 <li><img src="images/f_apple.png" /><span>苹果</span></li>
     6                 <li><img src="images/f_lemon.png" /><span>柠檬</span></li>
     7                 <li><img src="images/f_peach.png" /><span>桃子</span></li>
     8                 <li><img src="images/f_watermelon.png" /><span>西瓜</span></li>
     9         </ul>
    10 </div>

    其中:

    • zl_combobox:代表整个Web控件。
    • cb_input:结果显示框。
    • cb_btn: 下拉框按钮。
    • item_list:下拉框。下拉框中的“项”用<li>元素来表示。

    CSS样式表设计

    接下来我们要根据控件最终的效果图和前面设计的HTML代码,来完成样式表的设计。在设计样式表时,一般分为两个部分:

    • 基础外观:包括各个子部件的部件、关联关系、呈现外观等。
    • 状态样式:要考虑到控件在交互过程中的状态变化,通过‘开关’来实现不同状态下的外观,方便后续的JavaScript的控制。

    基础外观的相关CSS代码(zlbox.css)如下:

     1 .zl_combobox{
     2     position:relative;
     3     width:180px;
     4     font-size:1.5em;
     5     margin-left:5px;
     6     color:#323232;
     7 }
     8 .zl_combobox .cb_input{
     9     width:170px;
    10     height:60px;
    11     padding:0px 5px;
    12     line-height:60px;
    13     border:1px #A9A9A9 solid;
    14     background-color:#F2F2F2;
    15     cursor:pointer;
    16 }
    17 .zl_combobox .cb_input:after,
    18 .zl_combobox .item_list li:after{
    19     content:'';
    20     clear:both;
    21     display:table;
    22 }
    23 .zl_combobox .cb_input span,
    24 .zl_combobox .item_list li span{
    25     display:block;
    26     padding-left:10px;
    27     height:60px;
    28     line-height:60px;
    29 }
    30 .zl_combobox .cb_input img,
    31 .zl_combobox .item_list li img{
    32     display:block;
    33     width:50px;
    34     height:50px;
    35 }
    36 .zl_combobox .cb_input>img,
    37 .zl_combobox .cb_input>span,
    38 .zl_combobox .item_list li>img,
    39 .zl_combobox .item_list li>span
    40 {
    41     float:left;
    42 }
    43 .zl_combobox .cb_btn{
    44     position:absolute;
    45     right:0px;
    46     top:1px;
    47     width:60px;
    48     height:60px;
    49     background:url('img/cb_btn_down.png') no-repeat center center;
    50     cursor:pointer;
    51 }
    52 .zl_combobox .item_list{
    53     display:none;
    54     position:absolute;
    55     right:0px;
    56     top:62px;
    57     width:180px;
    58     line-height:60px;
    59     background-color:#FEFEFE;
    60     z-index:999;
    61 }
    62 .zl_combobox .item_list li{
    63     padding-left:10px;
    64     border-bottom:2px #CCCCCC dotted;
    65     cursor:pointer;
    66 }

    因为本文重点在于介绍Web控件的设计过程,关于CSS设计的细节,就不展开细讲,如有疑问,咱们在评论中交流讨论。

    那么,整个控件在交互过程中,会有哪些状态需要考虑呢?

    1. 默认的状态:不显示下拉框,仅仅显示“结果显示框”和“下拉框按钮”。

    2. 下拉框展开状态:显示下拉框,并且下拉框按钮的箭头变为向上。

    3. 选项的鼠标悬停状态:当鼠标悬停到选项时,改变不同的样式。

    (当然,如果你确定你的控件只是在手机/Pad上使用,可以不考虑鼠标悬停)。

    4. 被选中的选项的状态:这个可以根据需要设计。

    状态相关的样式的CSS代码(zlbox.css)如下:

    1 .zl_combobox.selected .item_list{
    2     display:block;
    3 }
    4 .zl_combobox.selected .cb_btn{
    5     background:url('img/cb_btn_up.png') no-repeat center center;
    6 }
    7 .zl_combobox .item_list li:hover{
    8     color:#5281F8;
    9 }

    注意到,我们通过判断控件主体.zl_combobox是否有selected的class来控制下拉框的显示状态。

    至此,控件的外观部分设计已经完成,我们可以看到,Web控件的外观设计有以下几个特点:

    a. 布局属性:Web控件可能在各种场合中使用,所以,我们一般不对控件主体的布局属性进行设置(举例中我们仅仅设置了margin-left:5px,约定控件在使用过程中,距离它前面的控件为5px)。

    b. 基础属性:像字体大小、字体颜色这样基础属性,在控件主体中尽量设置一下,确保控件的风格可以得到保证。比如:常规情况下,背景色是白色的,而字体的颜色是黑色,这时候,黑色的字体在白色背景的下拉框中可以显示出来。假设你没有设置控件主体的字体颜色,而在某个使用环境中,背景变成了黑色,字体颜色设置为白色,由于样式具有继承的特性,这时候就会变成在白色下拉框中的字体颜色是白色的。所以,在控件主体中设置基础属性,可以有效避免外部环境的“入侵”,让控件的风格自成一体。

    实现控件的基本框架

    工程文件组织

    尽管前面我们已经讲解了控件的外观设计,也写了相关的HTML代码和CSS代码,但其实有一个本来应该先做的事情没有完成,就是工程文件组织,如下所示:

    控件主文件夹的说明如下:

    • css:保存项目用到的CSS相关的文件。
    • images:保存需要在项目中直接引用的图片资源。
    • js:保存项目用到的JavaScript相关的文件。
    • index.html:HTML主体文件。

    CSS文件夹下的内容:

    • img:保存在css样式表中要引用的背景图片资源。
    • index.css:与工程相关的样式表,比如:页面布局等。
    • reset.css:一个重置样式表,将HTML中元素的标准特性重置。
    • zlbox.css:与控件相关的样式表。

    JS文件夹下的内容:

    • jquery.zlbox.js:与控件相关的JavaScript文件,因为我们用到了jQuery库,所以以jquery为前缀。
    • jquery.1.7.1.js:jQuery库文件。

    本来应该还有一个index.js,用于保存工程相关的js代码,考虑到我们的演示demo的内容比较简单,工程相关的js代码调用就直接放到index.html文件中。

    关于工程文件的组织和命名,各个公司应该有各自的规范标准,实际使用过程中,请遵照公司的规范。本文的论述将按以上的组织来进行。

    JavaScript代码设计

    总算到了我们Web控件设计的核心环节,我们的控件是基于jQuery库来设计的。jQuery插件的设计方法有很多种,我们依据我们的业务特点,选用了下面的基本框架模式,核心代码如下:

     1 $.fn.czl_combobox = function( options )
     2     {
     3        this.each( function()  
     4        {
     5           var instance = $.data( this , 'czl_combobox' );
     6           if( !instance )
     7           {
     8             $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) );
     9           }
    10           
    11        });//end of each
    12        
    13        return this;  
    14 };
    15 $.ZLComboBox = function( options , element )
    16 {    
    17     this.$el= $( element ); 
    18     this._init( options );
    19 };
    20 $.ZLComboBox.defaults = {
    21     
    22 };
    23 $.ZLComboBox.prototype = {   
    24 _init : function( options ) {
    25    //初始化函数
    26 },
    27 _loadEvents:function( ){
    28   //控件相关的事件注册
    29 }
    30   };

    我们先来看一下调用这个Web控件的代码,然后我们对照起来分析:

    $('.zl_combobox').czl_combobox( {} );

    让前端的HTML元素和后端的JavaScript代码关联起来,czl_combobox 这个函数是关键,所以,我们就从$.fn.czl_combobox这个函数入手,来看看它到底是如何关联的,先看代码:

     1 $.fn.czl_combobox = function( options )
     2  {
     3        this.each( function()  
     4        {
     5           var instance = $.data( this , 'czl_combobox' );
     6           if( !instance )
     7           {
     8             $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) );
     9           }
    10           
    11        });//end of each
    12        
    13        return this;  
    14 };

    1. czl_combobox是$.fn的成员,意味着凡是jQuery对象都可以调用它。

    2. 通过$.data()获取jQuery对象(对应一个HTML元素)的名称为czl_combobox的数据对象。

       如果不存在,那么,就通过$.data()新建一个ZLComboBox 的对象。

       如果已存在,那么,就不重复创建对象。

    3. 创建对象时,把当前jQuery对象和参数options传入。

    经过分析,我们发现$.fn.czl_combobox也仅仅是桥梁,还不是控件的核心,真正的核心是ZLComboBox对象,这个对象可以通过对应的HTML元素访问到。

    现在,我们依据控件的业务特征,先来预估一下ZLComboBox对象能做什么:

    1. 既然将HTML元素对应的DOM对象作为参数传给ZLComboBox对象,那么,ZLComboBox对象就能对相关的HTML元素以及它的子元素进行操作。

    2. 同时将参数options传递给ZLComboBox对象,意味着,可以根据业务需要对ZLComboBox对象进行一些”定制”操作,包括:属性和行为。

    下面我们再来看一下ZLComboBox的核心代码:

    1. 在它的构造函数中,将传入的HTML DOM对象保存到自己的一个属性成员中,并且将参数options传递给自己的一个初始化方法(_init())。

    2. 在它的原型中,定义了_init() 和 _loadEvents()两个方法。

    我们的目的是要让控件能够响应’单击’事件,并且对控件的各个子部件进行状态的改变,所以我们可以这样操作:

    1. 在_init中,通过控件主体的 DOM对象,获得它的各个子部件的jQuery对象。

    2. 在_loadEvents中,对子部件的jQuery对象绑定响应事件。

    3. 增加一个属性selected_index,记录当前选中的项的序号。

    4. 定义一个方法_show_itemlist,用来操作下拉框的显示和隐藏。

    完整的JS代码(zlbox.js)如下,可对照注释进行理解:

     1 (function($){
     2 $.fn.czl_combobox = function( options )
     3     {
     4        this.each( function()  
     5        {
     6           var instance = $.data( this , 'czl_combobox' );
     7           if( !instance )
     8           {
     9             //主体功能通过一个对象实现 
    10             $.data( this, 'czl_combobox' , new $.ZLComboBox( options , this ) );           
    11  }
    12         });//end of each
    13        
    14        return this;  
    15     };
    16     $.ZLComboBox = function( options , element )
    17     {    
    18         this.$el= $( element ); 
    19         this._init( options );
    20     };
    21      
    22     $.ZLComboBox.prototype = {   
    23           _init : function( options )
    24           {
    25             //相关的控件
    26             this.comboBox = this.$el ;
    27             
    28             //相关的HTML控件
    29             this.cb_btn = this.comboBox.children( '.cb_btn' ).eq(0);
    30             this.cb_input = this.comboBox.children( '.cb_input' ).eq(0);
    31             this.cb_item = this.comboBox.find( 'li' );
    32 
    33             //控件的状态,标记是否显示下拉列表
    34             this.cb_showitem_status = false ;     
    35             
    36             //初始化默认的值,默认选中最后一个选项
    37             this.selected_index = this.cb_item.length-1 ;                       
    38             this.cb_input.html( this.cb_item.eq(this.selected_index).html() );  
    39 
    40             //注册响应事件
    41             this._loadEvents();
    42           },
    43          _show_itemlist:function(){
    44                 if( this.cb_showitem_status === false ){
    45                     this.comboBox.addClass( 'selected' );
    46                     this.cb_showitem_status = true;
    47                 }
    48                 else{
    49                     this.comboBox.removeClass( 'selected' );
    50                     this.cb_showitem_status = false;
    51                 }
    52                 return ;
    53          },
    54          _loadEvents:function(){
    55             var _self = this;   
    56             //1_单击下拉箭头
    57             this.cb_btn.on( 'click' , function( event ){ 
    58                 _self._show_itemlist( );
    59                 return ;
    60             });
    61             //2_单击编辑框,也同样进行下拉框的状态切换
    62             this.cb_input.on( 'click' , function( event ){ 
    63                 _self._show_itemlist( );
    64                 return ;
    65             });
    66             //3_单击选项
    67             this.cb_item.on( 'click' , function( event ){ 
    68                 var index = $(this).index();
    69                 if( _self.selected_index !== index ){
    70                     //设置选项的值
    71                     _self.cb_input.html( $(this).html() );
    72                     
    73                     //设置当前选中的项
    74                     _self.selected_index = index ;
    75                     
    76                     //隐藏下拉框
    77                     _self._show_itemlist();
    78                 }
    79                 return ;
    80             });
    81          }
    82 };
    83 }(jQuery));

    插件验证:

    在index.html中,增加对插件调用的代码:

    <script type="text/javascript">
    $('.zl_combobox').czl_combobox( {} );
    </script>

    打开index.html网页,我们发现自定义的控件已经可以响应用户的单击事件,效果如下:

    优化控件的可扩展性

    有了前面的基础,接下来的理解就会比较简单。前面我们在分析控件的调用过程中,有一个参数options传到ZLComboBox对象中,但是,在最后的_init和_loadEvents方法中都没有用到过,那么,这个options的意义在哪里呢?

    我们还是从业务的需求出发,然后再来考虑我们如何实现:

    • 如果将控件用在“幼儿教学”的应用中,用户选中一种水果之后,可以播放与这种水果相关的介绍视频。
    • 如果将控件用在“卖水果”的应用中,用户选中一种水果,可以显示这种水果的价格、产地等信息。

    这就意味着,在不同的应用场景中,用户做出“选择水果”的操作时,触发的后续动作是不一样的,我们的控件应该支持这种操作才对。

    如果在初始化控件的时候,传入一个“回调函数”,当事件触发时,调用一下这个传入的回调函数,那不就达到我们的目的了吗?这样,不同的业务场景,我们只要传入不同的回调函数即可。

    回调函数是JavaScript的最拿手的,现在,我们就来优化我们的控件,让它支持回调函数。

    这里也先假设一个场景:

    当用户选中一个选项之后,我们就将选项中水果的图片显示出来,这时候,调用这个控件的方式变成这样:

    1 $('#fruit_box').czl_combobox( {
    2             selectItemEvent:function( index ){
    3                 var html_content = $( '.item_list li img' ).get( index ).outerHTML;
    4                 $('#result_box').html( html_content );
    5             }
    6 } );

    注意到两点:

    1. #fruit_box 为控件. zl_combobox对应的id,在同一个应用中可能存在多个zl_combobox控件,当要传入回调函数时,一般就用id去引用对应的控件元素,因为相同的控件,在不同的应用场景,行为是不同的,传入的回调函数也应该不同,所以不建议通过$(‘. zl_combobox’)一次性对所有的控件进行初始化。

    2.传入的回调函数的名称是selectItemEvent,带有一个index 的参数。

    现在,我们来优化控件的核心对象ZLComboBox中的内容:

    1. 新增一个options成员,用来保存传入的参数对象:

    _init:function( options ){
         //
         this. options = options ;
    }

    2. 在下拉列表框中的选项单击事件中,增加对回调函数的调用:

           //….
           //隐藏下拉框
        _self._show_itemlist();
        //调用回调函数
        _self.options.selectItemEvent( index );

    然后,我们再来验证一下效果,打开index.html,从下拉框中选中一个选项,效果如下:

    至此,我们最初设定的目的是已经达成了。为了完整性,再补充一下控件“扩展性”的另外一个特征:默认值设定。

    还是以之前的业务场景为例:如果用户没有传入回调函数,我们希望控件就把选中项的序号通过提示框显示出来。如果有传入会调用函数,就调用用户传入的回调函数。也就是说,给控件增加默认的行为,当调用时没有传入指定参数,就调用默认的值

    现在我们就来实现控件的默认值设定:

    1. 给ZLComboBox对象增加defaults成员,代码如下:

    1 //默认值设置
    2     $.ZLComboBox.defaults = {
    3         selectItemEvent:function( index ){
    4             alert( index );
    5             return ;
    6         }
    7     };

    2. 在_init方法中,保存传入的options对象采用如下的方式:

    this.options = $.extend( true , {} , $.ZLComboBox.defaults , options );

    这样,如果options中没有指定相关的成员,就调用defaults中的成员。这是jQuery插件处理传入参数的一种方式,$.extend为jQuery库定义的方法。 

    优化控件的可用性

    从理解语言特性来看,这部分内容与上一部分没有什么差异,我们只是从业务角度来看,对实现方式做一些区分。

    回到我们控件的场景,本文实现的控件我们取名为ZLComboBox,ZL为前缀,显然,它的行为特征与标准的组合框控件是类似的,组合框控件最常见的使用方式就是在表单(Form)中,那么,我们的控件也应该支持在表单(Form)中使用。

    如果ZLComboBox是作为表单的一个组件,那么,就需要在用户提交表单数据时,能够获取到用户到底选了哪个选项。从目前来看,似乎只能判断.cb_input容器中的内容,而这个内容是这个样子:

    <img src="images/f_apple.png" /><span>苹果</span>

    显然,用户处理起来非常不方便。

    也许你已经想到了,可以通过传入回调函数的方式,当用户选中一个选项之后,就将这个序号值(index)更新到某个隐藏的<input>元素中,表单提交数据时,提交这个隐藏的<input>中的值即可,这当然是一种处理方式。

    其实,我们可以给控件的核心对象ZLComboBox增加一个方法getSelectedIndex,用来获取当前用户选中的项的序号,代码如下:

    1 _loadEvents:function(){
    2             //
    3          },
    4          getSelectedIndex:function(){
    5                 return this.selected_index;
    6          }

    这时候,控件的调用方式变成:

    1 $('#fruit_box').czl_combobox( {
    2             selectItemEvent:function( index ){
    3                 var html_content = $( '.item_list li img' ).get( index ).outerHTML;
    4                 $('#result_box').html( html_content );
    5             }
    6 } );
    7 
    8 //在表单提交前的数据处理中,取得用户选中项的序号
    9 var index = $('#fruit_box').data( ‘czl_combobox’ ). getSelectedIndex() ;

    重点在第9行。

    当然,你也许会觉得,这种方式还不如之前动态更新隐藏<input>的方式自然,这里的主要目的是给大家一个特性解释,具体情况当然依据业务需要来确定。

    另外,也许你已经发现了,每次调用我们自定义的控件,在HTML代码中都要写入一大堆内容,能不能在使用时HTML代码中仅仅定义<div class=” zl_combobox”></div>?然后将选项通过参数options传入,在ZLComboBox的_init方法中动态生产子部件呢?答案是:当然可以,就留作练习吧。

    总结

    通过前面的内容介绍,我们大致理解了自定义控件的意义,以及开发一个Web自定义控件的大致过程,希望能给大家带来一些启发。当然,我们的举例,实现方式都仅仅是为了讲解整个过程,在实际的开发过程中,除了业务诉求之外,还需要考虑其他方面的要求,比如性能方面:将整个控件库中CSS样式用到的背景图片优化成"雪碧"图,减少加载时间。或者安全性的优化:将某些插件的框架由'伪类'(new)调整为'闭包'模式,增强控件的安全性。

    完整的示例代码,请单击此处下载。

    感谢诸位捧场^_^~~

  • 相关阅读:
    cf D. Vessels
    cf C. Hamburgers
    zoj 3758 Singles' Day
    zoj 3777 Problem Arrangement
    zoj 3778 Talented Chef
    hdu 5087 Revenge of LIS II
    zoj 3785 What day is that day?
    zoj 3787 Access System
    判断给定图是否存在合法拓扑排序
    树-堆结构练习——合并果子之哈夫曼树
  • 原文地址:https://www.cnblogs.com/alai88/p/5281602.html
Copyright © 2011-2022 走看看