开发背景:

        每次都很啰嗦,要先陈述一下开发背景。不过,这样做的目的一来是让自己能够快速回忆起自己的实战背景,二来是类似引子,能够阐述这样处理的实际用途,不仅仅是用于理论学习。

        今天记录下的是前几天开发的一个下拉框组件,该组件内容是一棵菜单树。本来系统开发的时候使用的是BUI,BUI本身也有下拉树组件。只是因为我们使用的BUI版本比较早,今年的版本和去年的在某些方面不兼容,如果一次性替换,会引起很多js报错。又因为近期使用过就是jstree,测试过几万条节点数据的加载性能,相比BUI,渲染速度有优秀很多(BUI并不能胜任这个数量级)。所以,干脆自己封装一个组件。具体要求,我总结了一下:

自定义下拉框,该下拉框内容为菜单树。下拉框操作规则同其他BUI下拉框组件,但是封装方式并非使用BUI封装方式,而是自定义封装。所以需要测试如下功能 
①菜单树节点被选中,会将值显示在输入框,满足多选功能,用逗号分隔 
②菜单树初始化会将存储的节点名称显示在输入框,并且菜单树相应节点会被触发选中 
③提供清空功能,点击清空按钮,下拉框所有选中节点都会清空,并且输入框值也清空;清空按钮在菜单树被选中的情况下显示,在没有选中的情况下隐藏 
④下拉框在单击输入框和下拉标签的情况下会显示出来,在单击下拉框区域外的所有位置都会自动收缩 
⑤查询界面的下拉框的清空功能和“重置”按钮联动 


实战结果:

        这里,有使用到很多封装细节,因为时间问题,初稿不做太多细节表述,重点在代码中有做注释。直接上代码:

首先前端html:

  1. <%@ page language="java" contentType="text/html; charset=UTF-8"
  2. pageEncoding="UTF-8"%>
  3. <%-- 显示输入框 --%>
  4. <div class="rmp-span">
  5. <label class="rmp-control-label">部门归属</label>
  6. <div class="rmp-control-text">
  7. <div id="department" class="bui-cncselectfilter bui-suggest bui-combox bui-select" aria-disabled="false" tabindex="0" hidefocus="true" aria-pressed="false">
  8. <input type="text" class="bui-select-input" /><span class="icon-delelte-input"><i class="icon-remove-sign-select"></i></span><span class="x-icon x-icon-normal"><span class="x-caret x-caret-down"></span></span>
  9. </div>
  10. </div>
  11. </div>
  12. <%-- 隐藏的存值输入框 --%>
  13. <input name="qo.department" id="rdepartment" type="hidden"/>
  14. <%-- 隐藏的下拉框 --%>
  15. <div id='jstree_around' class="hide">
  16. <div class="apm-treeselect-header">
  17. <div class="apm-treeselect-filter">
  18. <input id="search_input" class="filter"/>
  19. </div>
  20. </div>
  21. <div id="jstree_div"></div>
  22. </div>
  23. <link rel="stylesheet" href="${uics}/jstree/themes/default/style.css" />
  24. <script type="text/javascript" src="${uics}/jstree/jstree.js"></script>
  25. <script type="text/javascript">
  26. // AJAX异步拉取数据
  27. var treeData = null;
  28. $.ajax({
  29. url : ctx + "/common/organization-tree.action",
  30. type : "post", async: false,
  31. success : function(data) {
  32. treeData = data.tree;
  33. }
  34. });
  35. $(function(){
  36. //树主体初始化
  37. $('#jstree_div').jstree({
  38. "core" : {
  39. "multiple" : true, // 允许多选
  40. 'animation' : false,
  41. 'data' : treeData,
  42. },
  43. 'expand_selected_onload' : true, //选中项蓝色底显示
  44. 'checkbox' : {
  45. // 禁用级联选中
  46. 'three_state' : false,
  47. 'cascade' : 'undetermined' //有三个选项,up, down, undetermined; 使用前需要先禁用three_state
  48. },
  49. 'plugins' : ['checkbox', 'search'] //如果使用checkbox效率会降低, 'wholerow'会把线隐藏掉
  50. });
  51. //绑定到自定义的组件上
  52. ApmJstreeUtil.bindJstree({
  53. render : 'jstree_div',
  54. showField : 'department',
  55. saveField : 'rdepartment',
  56. picker : 'jstree_around',
  57. searchField : 'search_input',
  58. width : 250,
  59. height : 300
  60. });
  61. })
  62. </script>


js封装方式:

  1. var ApmJstreeUtil = {
  2. /**
  3. * @param render-渲染id,saveFiled-存储的input id,showField-展示的input id,
  4. * picker-下拉div的id
  5. */
  6. bindJstree : function(obj) { //输入对象
  7. //初始化下拉隐藏域的弹出位置和宽高
  8. var picker = $('#' + obj['picker']);
  9. picker.addClass('apm-tree-picker');
  10. picker.css('width', obj['width']);
  11. picker.css('height', obj['height']);
  12. var inputDiv = $('#' + obj['showField']);
  13. var top = inputDiv.offset().top + inputDiv.outerHeight(); //获取偏移位置
  14. var left = inputDiv.offset().left;
  15. picker.css('top', top); //设置绝对位置
  16. picker.css('left', left);
  17. var treeObj = $('#' + obj['render']);
  18. treeObj.css("text-align", "left");
  19. var deleteIcon = inputDiv.find("i"); //通过find查找子元素
  20. var inputShow = inputDiv.find("input");
  1. var saveInput = $('#' + obj['saveField']);
  2. //默认查询框,可以外部自定义,从而覆盖该触发方式
  3. if (obj['searchField'] && $('#' + obj['searchField'])) {
  4. var searchFieldObj = $('#' + obj['searchField']);
  5. var to = false;
  6. searchFieldObj.keyup(function() { //绑定按键事件,也可以绑定特定按键
  7. if (to) {
  8. clearTimeout(to);
  9. }
  10. to = setTimeout(function() {
  11. var v = searchFieldObj.val();
  12. treeObj.jstree(true).search(v);
  13. }, 250);
  14. });
  15. }
  16. //将选择值显示在输入input和隐藏input
  17. treeObj.on("changed.jstree", function(e, data) {
  18. if (data && data.selected.length > 0) {
  19. $('#' + obj['saveField']).val(data.selected.join(","));
  20. var i, j, r = [];
  21. for(i = 0, j = data.selected.length; i < j; i++) {
  22. r.push(data.instance.get_node(data.selected[i]).text);
  23. }
  24. deleteIcon.show();
  25. inputShow.val(r.join(","));
  26. } else {
  27. deleteIcon.hide();
  28. inputShow.val("");
  1. saveInput.val("");
  2. }
  3. });
  4. // 绑定load时间,初始化数据显示
  5. treeObj.on("loaded.jstree", function(e, data) {
  6. treeObj.jstree("open_all"); //展开全部
  7. var saveValue = $('#' + obj['saveField']).val();
  8. var checkNodeIds = saveValue.split(",");
  9. if (!saveValue || !checkNodeIds || checkNodeIds.length === 0) {
  10. deleteIcon.hide();
  11. return ;
  12. }
  13. var r = [];
  14. treeObj.find("li").each(function() {
  15. for (var i = 0; i < checkNodeIds.length; i++) {
  16. if ($(this).attr("id") == checkNodeIds[i]) { //如果节点的ID等于checkNodeIds[i],表示要选中
  17. r.push(data.instance.get_node(checkNodeIds[i]).text);
  18. treeObj.jstree("select_node", $(this)); //选中的节点,不是check_node
  19. //$(this).children("a").addClass("jstree-clicked");
  20. break;
  21. }
  22. }
  23. })
  24. deleteIcon.show();
  25. inputShow.val(r.join(","));
  26. });
  27. //隐藏和展示绑定
  28. inputDiv.on('click', function() {
  29. picker.show();
  30. });
  31. $('body').click(function(evt) {
  32. if ($(evt.target).parents('#' + obj['showField']).length == 0
  33. && $(evt.target).parents('#' + obj['picker']).length == 0 //判断鼠标点击的上层是否是#jstree_div
  34. && evt.target.id != obj['showField']
  35. && evt.target.id != obj['picker']
  36. && evt.target.className.indexOf("jstree") == -1) { //防止点击展开节点前面值为true
  37. picker.hide();
  38. }
  39. });
  40. //清空按钮
  41. deleteIcon.on('click', function() {
  42. ApmJstreeUtil.deselectJstree(obj);
  43. deleteIcon.hide();
  44. });
  45. },
  46. /**
  47. * 清楚被选中的项
  48. * @param render-渲染id,saveFiled-存储的input id,showField-展示的input id,
  49. * picker-下拉div的id
  50. */
  51. deselectJstree : function(obj) {
  52. var treeObj = $('#' + obj['render']);
  53. var saveField = $('#' + obj['saveField']);
  54. var checkNodeIds = saveField.val().split(",");
  55. if (!checkNodeIds || checkNodeIds.length === 0) {
  56. return ;
  57. }
  58. treeObj.find("li").each(function() {
  59. for (var i = 0; i < checkNodeIds.length; i++) {
  60. if ($(this).attr("id") == checkNodeIds[i]) {
  61. treeObj.jstree("deselect_node", $(this)); //取消选中的节点
  62. break;
  63. }
  64. }
  65. })
  1. saveInput.val("");
  2. }
  3. };<strong>
  4. </strong>

效果展示:

单击输入框,弹出下拉框,使用jstree生成的树                  选中数据后,会显示叉形删除标示,可以清空数据,包括清空隐藏域

                      


待改进

可以发现,前端html引用输入框和隐藏下拉框代码太长,不便他人二次使用。应该考虑进一步封装到ApmJstreeUtils中。


小结:

        这应该可以算是我第一次比较大的js封装实战。以前对于事件的绑定、DOM树的细节操作都不是很清楚,实战后对这些有了进一步的理解。js是个非常灵活的弱语言,开发方式非常多,从而对于初学者而言,有弊有利——弊是别人封装的很多代码不是那么好理解,除非一步步深究,还有就是很多细节用得不是很合理,(详见《JavaScript语言精粹》),容易留下鲁棒性不足的隐患;利,最明显的就是,不管你是不是菜鸟,了解一点js,都能搞点可以显摆给外行人看的交互界面。