BootStrap-DualListBox能夠實現將所選擇的列表項顯示到右邊,未選的列表項顯示到左邊。
但是左右兩邊的下拉框中都是單級列表。如果要實現將兩邊都是樹(縮進樹),選擇某個節點時,其子節點也進到右邊,不選某個節點時,其子節點也都回到左邊呢?
實現思路是:
1、在DualListBox每次選擇時,都會觸發change事件,我們在change中,去處理子節點的選擇和未選擇。所有處理都通過change事件觸發。
2、在處理完后,調用DualListBox的refresh方法。
在具體處理中,需要遍歷樹的節點數據,來獲取樹節點,子節點,父節點,並進行遞歸處理。
為了方便調用,將改進后擴展的代碼放到單獨的文件中,並擴展了jquery方法,增加BootDualTree方法,實現雙樹的初始化,加載數據,獲取選中值,反向綁定等方法。
調用代碼示例如下:查看在線演示
1 <head> 2 <title>Bootstrap Dual Listbox</title> 3 <link href="bootstrap.min.css" rel="stylesheet"> 4 <!--<link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">--> 5 <link rel="stylesheet" type="text/css" href="../src/prettify.css"> 6 <link rel="stylesheet" type="text/css" href="../src/bootstrap-duallistbox.css"> 7 <script src="jquery.min.js"></script> 8 <!--<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>--> 9 <script src="bootstrap.min.js"></script> 10 11 <!--<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>--> 12 <script src="../src/jquery.bootstrap-duallistbox.js"></script> 13 <script src="bootstrap-dualtree.js"></script> 14 </head> 15 <body class="container"> 16 17 18 19 <h2>lh Test</h2> 20 <p> 21 Make the dual listbox be dual tree. 22 </p> 23 <div> 24 <form id="lhdemoform" action="#" method="post"> 25 <select multiple="multiple" size="10" name="duallistbox_lhdemo"> 26 <!--<option value="option1">Option 1</option>--> 27 </select> 28 <br> 29 <input type="button" value="初始數據" id="btnAddNew" /> 30 <input type="button" value="獲取選中數據" id="btnAddNew1" onclick="alert($('select[name=duallistbox_lhdemo]').bootstrapDualTree('getSelValues',false).join(' '))"/> 31 <input type="button" value="獲取選中葉子節點數據" id="btnAddNew2" onclick="alert($('select[name=duallistbox_lhdemo]').bootstrapDualTree('getSelValues').join(' '))" /> 32 <select multiple="multiple" size="10" name="duallistbox_lhdemo2"> 33 <!--<option value="option1">Option 1</option>--> 34 </select> 35 <input type="button" value="獲取選中數據" id="btnAddNew3" onclick="alert($('select[name=duallistbox_lhdemo2]').bootstrapDualTree('getSelValues',false).join(' '))" /> 36 <input type="button" value="獲取選中葉子節點數據" id="btnAddNew4" onclick="alert($('select[name=duallistbox_lhdemo2]').bootstrapDualTree('getSelValues').join(' '))" /> 37 </form> 38 <script> 39 40 //調用示例 41 var data = { 42 text: "t1", 43 value: "v1", 44 pid: "0", 45 children: [ 46 { 47 text: "t11", 48 value: "v11", 49 pid: "v1", 50 children: [ 51 { 52 text: "t111", 53 value: "v111", 54 pid: "v11", 55 }, 56 { 57 text: "t112", 58 value: "v112", 59 pid: "v11", 60 children: [ 61 { 62 text: "t1121", 63 value: "v1121", 64 pid: "v112", 65 }, 66 { 67 text: "t1122", 68 value: "v1122", 69 pid: "v112", 70 }, 71 ], 72 }, 73 ] 74 }, 75 { 76 text: "t12", 77 value: "v12", 78 pid: "v1", 79 children: [ 80 { 81 text: "t121", 82 value: "v121", 83 pid: "v12", 84 }, 85 { 86 text: "t122", 87 value: "v122", 88 pid: "v12", 89 }, 90 ] 91 }, 92 ], 93 }; 94 95 var lhdemo = $('select[name="duallistbox_lhdemo"]').bootstrapDualTree({ 96 nonSelectedListLabel: '未選', 97 selectedListLabel: '已選', 98 preserveSelectionOnMove: 'moved', 99 moveOnSelect: true, 100 //dualTree在dualListbox基礎上新增的屬性 101 data: data,//樹形節點數據 102 selValues: ["v1121"], //默認選中節點值,為數組.如果不傳,則默認不選中 103 indentSymbol: "-" //縮進符號,默認為- 104 }); 105 var lhdemo2 = $('select[name="duallistbox_lhdemo2"]').bootstrapDualTree({ 106 nonSelectedListLabel: '未選', 107 selectedListLabel: '已選', 108 preserveSelectionOnMove: 'moved', 109 moveOnSelect: true, 110 //dualTree在dualListbox基礎上新增的屬性 111 data: data,//樹形節點數據 112 selValues: ["v1121", "v1122"], //默認選中節點值,為數組.如果不傳,則默認不選中 113 indentSymbol: "-" //縮進符號,默認為- 114 }); 115 $("#btnAddNew").click(function () { 116 //lhdemo.bootstrapDualTree("loadData", data, ["v1121", "v1122"]);//加載數據方法,可同時傳遞當前選中值 117 lhdemo.bootstrapDualTree("setValues", ["v1121", "v1122"]);//設置當前選中值 118 }); 119 120 121 </script> 122 </div> 123 </body>
效果如下:
打包的bootstrap-dualtree.js文件代碼如下:
1 /** 2 * bootstrapDualTree extended from bootstrapDualListbox 3 * author: lh 2015-12-10 4 */ 5 (function ($, window, document, undefined) { 6 var pluginName = "bootstrapDualTree";//插件名稱 7 //擴展jquery方法 8 $.fn[pluginName] = function (options) { 9 var returns; 10 var args = arguments; 11 if (options === undefined || typeof options === 'object') { 12 return this.each(function () { 13 if (!$.data(this, "plugin_" + pluginName)) { 14 $.data(this, "plugin_" + pluginName, new BootstrapDualTree(this, options)); 15 } 16 }); 17 } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 18 this.each(function () { 19 var instance = $.data(this, 'plugin_' + pluginName); 20 // Tests that there's already a plugin-instance and checks that the requested public method exists 21 if (instance instanceof BootstrapDualTree && typeof instance[options] === 'function') { 22 // Call the method of our plugin instance, and pass it the supplied arguments. 23 returns = instance[options].apply(instance, Array.prototype.slice.call(args, 1)); 24 } 25 }); 26 }; 27 return returns !== undefined ? returns : this; 28 } 29 //定義DualTree對象 30 function BootstrapDualTree(element, options) { 31 32 var $e = $(element).bootstrapDualListbox(options); 33 this.tElement = $e; 34 35 if (options.data) { 36 this.data = options.data; 37 } 38 if (options.indentSymbol!==undefined) { 39 this.setting.indentSymbol = options.indentSymbol; 40 } 41 if (options.selValues) { 42 this.selValues = options.selValues; 43 } 44 this.init(); 45 var dualTree = this; 46 //bootstrap dual-listbox 在其發生變化的時候,觸發change事件,實現雙樹都在這個事件中處理 47 $e.change(function () { 48 dualTree.refresh(); 49 }); 50 } 51 //定義可對外提供的方法 52 BootstrapDualTree.prototype = { 53 tElement:{},//select元素 54 data :{},//數據 55 selValues:[],//選擇的節點值 56 setting:{ 57 indentSymbol: "-", 58 }, 59 lastOptions :[],//用於記錄上一次的下列列表狀態,以便通過比較識別移動操作的目標節點有哪些 60 loadData: function (dataL, selValuesL) { 61 data = dataL; 62 selValues = selValuesL || []; 63 this.init(); 64 }, 65 setValues: function (selValuesL) { 66 selValues = selValuesL || []; 67 this.init(); 68 }, 69 getSelValues: function (onlyLeaf) { 70 if (typeof(onlyLeaf)== "undefined") onlyLeaf = true; 71 var selValues1 = getSelValues(this.tElement,this.data, onlyLeaf); 72 return selValues1; 73 }, 74 init: function () { 75 //alert(tElement) 76 this.tElement.find("option").remove(); 77 showData(this.tElement, this.data, this.indentSymbol, 0, this.selValues); 78 recLastOptions(this.tElement, this); 79 this.tElement.bootstrapDualListbox("refresh"); 80 if (this.selValues.length > 0) { 81 updateTreeSelectedStatus(this.tElement,this,this.data, this.selValues); 82 } 83 }, 84 refresh: function () { 85 updateTreeSelectedStatus(this.tElement,this, this.data); 86 } 87 88 }; 89 90 //獲取變化事件的方向:向右選擇,向左選擇 91 function getChangedDir() { 92 var dir = "all"; 93 var srcHtml = event.srcElement.outerHTML; 94 //arrow-right關鍵字針對點擊箭頭移動的情形,nonselected-list針對選中時直接移動的情形 95 if (/arrow-right/.test(srcHtml) || /nonselected-list/.test(srcHtml)) { 96 dir = "right"; 97 } 98 else if (/arrow-left/.test(srcHtml) || /selected-list/.test(srcHtml)) { 99 dir = "left"; 100 } 101 return dir; 102 } 103 //記錄上一個所有選項狀態 104 function recLastOptions(tElement,tTree) { 105 tTree.lastOptions = []; 106 tElement.find("option").each(function () { 107 var curNode = $(this); 108 tTree.lastOptions.push({ value: curNode.attr("value"), selected: curNode.prop("selected") }); 109 }); 110 } 111 //獲取發生變化的節點ID列表 112 function getChangedIds(tElement, lastOptions, dir) { 113 var changedIds = []; 114 if (dir == "right") {//向右,則取新選擇的節點 115 newOptions = tElement.find("option"); 116 for (var i = 0; i < newOptions.length; i++) { 117 if (newOptions[i].selected && !lastOptions[i].selected) 118 changedIds.push(lastOptions[i].value) 119 } 120 } 121 else if (dir == "left")//向左,則取新取消的節點 122 { 123 newOptions = tElement.find("option"); 124 for (var i = 0; i < newOptions.length; i++) { 125 if (!newOptions[i].selected && lastOptions[i].selected) 126 changedIds.push(lastOptions[i].value) 127 } 128 } 129 return changedIds; 130 } 131 132 //更新節點選中狀態,將選中節點的父節點也都選中; 133 function updateTreeSelectedStatus(tElement, tTree, data, selValues) { 134 var dir = selValues && selValues.length > 0 ? "right" : getChangedDir(); 135 var cIds = selValues || getChangedIds(tElement, tTree.lastOptions, dir); 136 console.log("changed:" + cIds) 137 if (dir == "right") { 138 //將所選節點的子節點及其路徑上的節點也選中 139 for (var i = 0; i < cIds.length; i++) { 140 var node = findNodeById(data, cIds[i]); 141 console.log("handling-right:") 142 console.log(node) 143 selAllChildNodes(tElement, node); 144 selAcesterNodesInPath(tElement,data, node); 145 } 146 } 147 else if (dir == "left") { 148 //將所選節點的子節點也都取消選中 149 for (var i = 0; i < cIds.length; i++) { 150 var node = findNodeById(data, cIds[i]); 151 console.log("handling-left:") 152 console.log(node) 153 unSelAllChildNodes(tElement, node); 154 unSelAcesterNodesInPath(tElement,data, node); 155 } 156 } 157 158 //重新添加未選節點及其父節點 159 //1、記錄未選節點及其父節點 160 var nonSelNodes = []; 161 tElement.find("option").not(":selected").each(function () { 162 var curNode = $(this); 163 nonSelNodes.push(curNode.attr("value")); 164 while (curNode.length > 0) { 165 var pOption = tElement.find("option[value='" + curNode.attr("rel") + "']"); 166 if (pOption.length > 0 && nonSelNodes.indexOf(pOption.attr("value")) < 0) nonSelNodes.push(pOption.attr("value")); 167 curNode = pOption; 168 } 169 }); 170 //2、清除未選擇的節點 171 tElement.find("option").not(':selected').remove(); 172 console.log("nonSelNodes:" + nonSelNodes) 173 //3、重新顯示左側下拉列表 174 showNonSelData(tElement, data, tTree.setting.indentSymbol, 0, nonSelNodes); 175 176 //重新顯示已選擇節點,以保持排序 177 var selNodes = []; 178 makeNoDuplicateSelNode(tElement); 179 var selOptions = tElement.find("option:selected"); 180 for (var n = 0; n < selOptions.length; n++) 181 selNodes.push(selOptions[n].value); 182 selOptions.remove(); 183 console.log("selNodes:" + selNodes) 184 showSelData(tElement, data, tTree.setting.indentSymbol, 0, selNodes); 185 186 tElement.bootstrapDualListbox("refresh"); 187 //記錄新的下拉框狀態 188 recLastOptions(tElement, tTree); 189 } 190 //遞歸顯示所有節點 191 function showData(tElement, node,indentSymbol, depth, selValues) { 192 var selValues = selValues || []; 193 var withdraw = ""; 194 for (var i = 0; i < depth; i++) 195 withdraw += indentSymbol; 196 tElement.append("<option value='" + node.value + "' rel='" + node.pid + "' " + (selValues.indexOf(node.value) >= 0 ? "selected" : "") + ">" + withdraw + node.text + "</option>"); 197 if (node.children) { 198 for (var n = 0; n < node.children.length; n++) { 199 showData(tElement, node.children[n],indentSymbol, depth + 1, selValues); 200 } 201 } 202 } 203 //遞歸顯示未選擇節點 204 function showNonSelData(tElement, node, indentSymbol, depth, nonSelNodes) { 205 var withdraw = ""; 206 for (var i = 0; i < depth; i++) 207 withdraw += indentSymbol; 208 if (nonSelNodes.indexOf(node.value) >= 0 && tElement.find("option[value='" + node.value + "']").not(":selected").length == 0) { 209 tElement.append("<option value='" + node.value + "' rel='" + node.pid + "'>" + withdraw + node.text + "</option>"); 210 if (node.children) { 211 for (var n = 0; n < node.children.length; n++) { 212 showNonSelData(tElement, node.children[n],indentSymbol, depth + 1, nonSelNodes); 213 } 214 } 215 } 216 } 217 //遞歸顯示已選擇節點 218 function showSelData(tElement, node, indentSymbol, depth, selNodes) { 219 var withdraw = ""; 220 for (var i = 0; i < depth; i++) 221 withdraw += indentSymbol; 222 if (selNodes.indexOf(node.value) >= 0 && tElement.find("option[value='" + node.value + "']:selected").length == 0) { 223 tElement.append("<option value='" + node.value + "' rel='" + node.pid + "' selected>" + withdraw + node.text + "</option>"); 224 if (node.children) { 225 for (var n = 0; n < node.children.length; n++) { 226 showSelData(tElement, node.children[n], indentSymbol,depth + 1, selNodes); 227 } 228 } 229 } 230 } 231 //去掉已選擇的重復節點 232 function makeNoDuplicateSelNode(tElement) { 233 tElement.find("option:selected").each(function () { 234 var curNode = $(this); 235 var options = tElement.find("option[value='" + curNode.attr("value") + "']:selected"); 236 if (options.length > 1) { 237 for (var i = options.length; i > 0; i--) 238 $(options[i]).remove(); 239 } 240 }); 241 } 242 //如果一個節點選擇了,則選中其子節點 243 function selAllChildNodes(tElement, node) { 244 if (node.children) { 245 for (var n = 0; n < node.children.length; n++) { 246 tElement.find("option[value='" + node.children[n].value + "']").prop("selected", true); 247 selAllChildNodes(tElement, node.children[n]); 248 } 249 } 250 } 251 //如果一個節點取消選擇了,則取消選中其子節點 252 function unSelAllChildNodes(tElement, node) { 253 if (node.children) { 254 for (var n = 0; n < node.children.length; n++) { 255 tElement.find("option[value='" + node.children[n].value + "']").prop("selected", false); 256 unSelAllChildNodes(tElement, node.children[n]); 257 } 258 } 259 } 260 //獲取選中的值列表 261 function getSelValues(tElement, node, onlyLeaf) { 262 var selValuesTmp = []; 263 tElement.find("option[value='" + node.value + "']").each(function () { 264 if ($(this).prop("selected")) { 265 if (!node.children || node.children.length == 0 || !onlyLeaf) { 266 selValuesTmp.push(node.value); 267 } 268 if (node.children) { 269 for (var n = 0; n < node.children.length; n++) { 270 selValuesTmp = selValuesTmp.concat(getSelValues(tElement,node.children[n], onlyLeaf)); 271 } 272 } 273 } 274 }); 275 return selValuesTmp; 276 } 277 //選中一個節點的路徑上的祖先節點 278 function selAcesterNodesInPath(tElement,root, node) { 279 var curNode = node; 280 while (curNode.pid != "0") { 281 curNode = findNodeById(root, curNode.pid); 282 var pOption = tElement.find("option[value='" + curNode.value + "']"); 283 if (pOption.length > 0) pOption.prop("selected", true); 284 } 285 } 286 //取消一個節點的路徑上的祖先節點,這些節點沒有子節點被選中 287 function unSelAcesterNodesInPath(tElement, root, node) { 288 var curNode = node; 289 while (curNode.pid != "0") { 290 curNode = findNodeById(root, curNode.pid); 291 if (!hasSelChildrenNodes(tElement, curNode)) { 292 var pOption = tElement.find("option[value='" + curNode.value + "']"); 293 if (pOption.length > 0) pOption.prop("selected", false); 294 } 295 } 296 } 297 //從樹中尋找某個id的節點 298 function findNodeById(node, id) { 299 if (node.value == id) { 300 return node; 301 } 302 else { 303 if (node.children) { 304 for (var i = 0; i < node.children.length; i++) { 305 var rsNode = findNodeById(node.children[i], id); 306 if (rsNode != null) return rsNode; 307 } 308 } 309 } 310 return null; 311 } 312 //判斷某個節點的子節點是否被選中 313 function hasSelChildrenNodes(tElement, node) { 314 if (node.children) { 315 for (var i = 0; i < node.children.length; i++) { 316 var pOption = tElement.find("option[value='" + node.children[i].value + "']:selected"); 317 if (pOption.length > 0) return true; 318 } 319 } 320 return false; 321 } 322 })(jQuery, window, document);