文涉及到combobox设计与实现上的一些方法论,并提供了一个参考实现.
Combobox(以下简称combox),也就是通常的意义上的Select控件,最具代表的是html里的select元素,
它可以在有限的空间内提供多个选项给用户选择,这个与菜单十分相似,并作为表单元素将数据提交到服务器.
种种原因使得有时有必要自定义一个combox控件,来代替自带的select,来分析一下html的select有什么基本功能.
>与form集成,字段可提交
>控件禁用或禁用单个选项
>选择,可以指定选择任意项或不选
>键盘导航
支持键盘上下键导航选项,ENTER,ESC键关闭下拉并响应
>滚动
如果列表项过多而下拉空间有限,就应该出现滚动条,
并且在用户选择选项过程中,选项要相应的滚动到可视范围内
>下拉框下拉时定位到可视范围内
>下拉框上下文切换时自动消失
即点击combox外时下拉框自动消失
如果是自定义的combox控件,可扩展功能有
>可编辑或不可编辑
>控件自定事件
>ajax加载
根据响应动态加载项
>过滤
用户在输入过程中过滤条项并自动定位
>定制下拉框
下拉框弹出的内容是可定制的,根据实现需要定制,如一个列表,一颗树,一个表格等
>在定制下拉框后combox的基本行为保持不应
自定下拉框后不能改变控件应用的基本行为,如选择,键导航等
>多样式支持,如控件的hover,点击,下拉时都有不同外观效果,下拉框具有阴影等
如果自定义combox控件,以上的基本功能是要实现的,不同方式有不同实现,越灵活就越不简单,.
现在考虑一个在封闭式环境下的实现,
这所谓的封闭环境是指为特定的需求应用而实现,代码考虑不重用,在响应过程中不与外界进行通讯,功能与控件完全一体化,具有很强的针对性实现,
这种实现较为简单,根据需求可部分实现select功能,有时为了设计与实现上的便利,用到多种的hack.
例如在HTML模板上的设计,可将下拉框的HTML组织在控制自身里,
而不是将下拉框的加在BODY中,这种设计简单明了,下拉定位方便,但也造成下拉框不够通用,在样式控制方面也要一定的技巧.
这类封闭环境下的产物常见有的一些自动完成控件,拿来就可以用.
下面来考虑一个通用的,可扩的设计方案
这里所谓通用,是指一个集成的环境里面,不同应用是共享的,代码是可重用的,来对上面combox所有功能分析一下,给控件解耦,提取出一些独立的功能,并给出一个与combox无关的控件设计模型.
一个控件模型可由以下一个或多个模型的组合而成
1.事件模型(event model)
具有事件模型的控件有添加事件监听器,发送,和移除事件的能力.
给控件建立事件模型是很有意义的,外部可监听控件一些感兴趣的事件并利用事件进行通讯,如MVC模型的实现多是基于事件(消息)的驱动.
实现了事件模型的控件具有可扩性,因为事件可有多个监听器,或者说通常意义上的回调,这个当然与单个回调效果不一样了.
有必要说说HTML事件模型与自定的不同,HTML受限于HTML结点而不是控件的事件,并且外界并不能向结点发送事件.
2.选择模型(selection model)
具有选择模型的控件具有单选,多选,并给合键盘导航选择等功能.
3.加载模型(loading model)
加载模型使得控件有自动Ajax加载并组装数据的能力
4.装饰模型(decoration model)
装饰模型与前面的几个模型不同,前面的通常是控件自身就支持,或是控件组成的不可或缺部分,而装饰模型不是控件自身就有的,它显得可有可无,只起来装饰效果,
而装饰时充分利用了其它的几个模型,例如在加载的时候给控件装饰一个”加载中”图标,利用事件模型作监听,利用加载模型加载前触发,在加载后移除效果.
此外不同应用还可独立出不同的模型,这里就不多说了.
通用的控件建好模型后,利用这些设计一个通用而功能齐全的combox控件.
combox控件根据视图划分为触发部分和下拉部分,其中下拉部分作为一个外部控件引入.
为了支持下拉的灵活实现,并且要求不同实现要具有一致的行为,下拉部分这控件必须支持上面所说的几个模型.
这样一来,Combobox就具有了大部分功能,并且行为始终保持一致.
实现参考:
http://www.bgscript.com/bgjs/samples/combo.html
讲一下实现参考例子里的内容:
里面的combox的下拉控件都实现了以上几种模型,
下拉框作为selector引入到combox,
selector在里面的分别是组控件,树型控件和小视图分组控件,
它们在combox中都表现出一致的行为,不加修改就可放到combox中.
下拉时出现loading图标和树型控件的loading图标是装饰模型实现后引起的效果,
它监听由加载模型触发的open,final事件并作出相应的处理.
代码:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>CCombox</title>
<script type="text/javascript" src="../bglib.js"></script>
<script type="text/javascript" src="../ui/shadow.js"></script>
<script type="text/javascript" src="../ui/container.js"></script>
<script type="text/javascript" src="../ui/loading.js"></script>
<script type="text/javascript" src="../ui/foldable.js"></script>
<script type="text/javascript" src="../more/folderview.js"></script>
<script type="text/javascript" src="../ui/group.js"></script>
<script type="text/javascript" src="../ui/form.js"></script>
<script type="text/javascript" src="../ui/combo.js"></script>
<script type="text/javascript">CTemplate.BLANK_IMG = '../default/s.gif';</script>
<script type="text/javascript" src="../ui/tree.js"></script>
<link rel="stylesheet" href="../default/global.css" type="text/css"/>
<link rel="stylesheet" href="../default/ru_share.css" type="text/css"/>
<link rel="stylesheet" href="resources/example.css" type="text/css"/>
<style>
.g-tree .g-panel-body, .g-folderview-wrap{border:1px solid #CCC;border-top:0;}
.g-folderview-ctx dd{margin:5px;}
</style>
</head>
<body lang="zh">
<p>
下拉框作为selector引入到combox,<br/>
selector在里面的分别是组控件,树型控件和小视图分组控件,<br/>
它们在combox中都表现出一致的行为,不加修改就可放到combox中.<br/>
下拉时出现loading图标和树型控件的loading图标是装饰模型实现后引起的效果,<br/>
它监听由加载模型触发的open,final事件并作出相应的处理.<br/>
在firebug下可察看事件
</p>
<div style="margin:20px 30px;position:relative;">
<h2>带滚动条,默认下拉,可编辑</h2>
<div id="def_combo"></div>
<h2>随内容伸缩,下拉,可编辑</h2>
<div id="auto_combo"></div>
<h2>禁用</h2>
<div id="disable_combo"></div>
<h2>不可编辑</h2>
<div id="unedit_combo"></div>
<h2>自定下拉控件和过滤方式</h2>
<div id="tree_combo"></div>
<h2>自定下拉控件和过滤方式 + 滚动 + Ajax动态加载</h2>
<div id="tree_ajax_combo"></div>
<h2>只要基于CSelectedContainer容器类的控件都可以</h2>
<div id="other_combo"></div>
</div>
</body>
</html>
<script>
Event.ready(function(){
//
// 默认下拉
//
var cb = new CCombox({showTo:'def_combo', autoRender:true,width:300,array:[
{title:'粉绿色',icon:'icoIbx'},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoIbx'},
{title:'清除记录',icon:'icoDel' ,disabled:true},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoNote'},
{title:'清除记录',icon:'icoDel'},
{title:'粉绿色',icon:'icoIbx'},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoDft'},
{title:'清除记录',icon:'icoDel' ,disabled:true},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoNote'},
{title:'清除记录',icon:'icoDel'}
]});
cb.select(2);
cb.setScrollorHeight(150);
cb = new CCombox({showTo:'auto_combo', autoRender:true,width:300,array:[
{title:'粉绿色',icon:'icoIbx'},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoIbx'},
{title:'清除记录',icon:'icoDel' ,disabled:true},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoNote'},
{title:'清除记录',icon:'icoDel'},
{title:'粉绿色',icon:'icoIbx'},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoDft'},
{title:'清除记录',icon:'icoDel' ,disabled:true},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoNote'},
{title:'清除记录',icon:'icoDel'}
]});
//不可编辑
cb = new CCombox({showTo:'unedit_combo', autoRender:true,uneditable:true, width:300,array:[
{title:'粉绿色',icon:'icoIbx'},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoIbx'},
{title:'清除记录',icon:'icoDel' ,disabled:true},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoNote'},
{title:'清除记录',icon:'icoDel'},
{title:'粉绿色',icon:'icoIbx'},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoDft'},
{title:'清除记录',icon:'icoDel' ,disabled:true},
{title:'粉红色',icon:'icoDft'},
{title:'蓝色',icon:'icoNote'},
{title:'清除记录',icon:'icoDel'}
]});
//禁用
cb = new CCombox({showTo:'disable_combo', disabled: true, autoRender:true,uneditable:true, width:300});
// ----------------------------------
// 下拉为树
//自定义过滤
function treeFilter(matcher, caller){
var caller = caller || window, fn = arguments.callee;
if(this.children){
var cm = true;
CC.each(this.children, (function(){
if(!matcher.call(caller,this) && !this.nodes){
this.display(0);
cm = false;
return;
}
this.display(1);
if(this.nodes && this.expanded)
fn.call(this, matcher, caller);
}));
}
}
var tree = new CTree({title:'tree', showTo:document.body, hidden:true,filter:treeFilter});
tree.root.fromArray([
{title:'disabled item',nodes:true,disabled:true},
{title:'B'},
{title:'C',nodes:true}
]);
tree.root.$(2).fromArray([
{title:'disabled A',nodes:true,disabled:true},
{title:'B',disabled:true},
{title:'items',nodes:true}
]);
tree.render();
//指定selector
cb = new CCombox({showTo:'tree_combo', selector:tree, autoRender:true,width:350});
tree = new CTree({title:'tree2', url:'/q?bg_q=test_group', autoConnect:true, showTo:document.body, hidden:true,filter:treeFilter});
tree.root.fromArray([
{title:'Back Light 点击展开', nodes:true},
{title:'BCDEFJDFL;AJSDL'},
{title:'disabled item',nodes:true,disabled:true},
{title:'BCDEFJDFL;AJSDL'},
{title:'C',nodes:true},
{title:'item something',disabled:true},
{title:'BCD人物事件'},
{title:'CDEFASLDFJ', nodes:true},
{title:'disabled item',nodes:true,disabled:true},
{title:'AAAAAAAAAAAAAA'},
{title:'WEB前端'}
]);
tree.root.$(2).fromArray([
{title:'disabled A',nodes:true,disabled:true},
{title:'B',disabled:true},
{title:'items',nodes:true}
]);
tree.render();
tree.root.expand();
//指定selector
cb = new CCombox({showTo:'tree_ajax_combo', autoRender:true,selector:tree, width:350});
cb.setScrollorHeight(150);
//
// 其它基于CSelectedContainer容器的控件
//
var foldview = new CFolderView({
showTo:document.body,
navKeyEvent : true,
hidden:true,
shadow:true,
title:'请选择其中一项',
autoRender:true
});
//指定selector
cb = new CCombox({showTo:'other_combo', selector:foldview, autoRender:true,width:320});
cb.selector.on('comboxshow', function(){
//content loaded
if(this.loaded){
return;
}
//方便显示loading..
this.setHeight(100);
this.on('load',(function(){
this.style('height','');
this.height = false;
}));
this.on('final',(function(){
//更新数量
this.setTitle(this.title);
}));
this.connect('http://www.bgscript.com/q?bg_q=foldview_data');
});
});
</script>