BootStrap-DualListBox能够实现将所选择的列表项显示到右边,未选的列表项显示到左边。

但是左右两边的下拉框中都是单级列表。如果要实现将两边都是树(缩进树),选择某个节点时,其子节点也进到右边,不选某个节点时,其子节点也都回到左边呢?

实现思路是:

1、在DualListBox每次选择时,都会触发change事件,我们在change中,去处理子节点的选择和未选择。所有处理都通过change事件触发。

2、在处理完后,调用DualListBox的refresh方法。

在具体处理中,需要遍历树的节点数据,来获取树节点,子节点,父节点,并进行递归处理。

为了方便调用,将改进后扩展的代码放到单独的文件中,并扩展了jquery方法,增加BootDualTree方法,实现双树的初始化,加载数据,获取选中值,反向绑定等方法。

调用代码示例如下:查看在线演示

   <head>
<title>Bootstrap Dual Listbox</title>
<link href="bootstrap.min.css" rel="stylesheet">
<!--<link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">-->
<link rel="stylesheet" type="text/css" href="../src/prettify.css">
<link rel="stylesheet" type="text/css" href="../src/bootstrap-duallistbox.css">
<script src="jquery.min.js"></script>
<!--<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>-->
<script src="bootstrap.min.js"></script> <!--<script src="http://libs.baidu.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>-->
<script src="../src/jquery.bootstrap-duallistbox.js"></script>
<script src="bootstrap-dualtree.js"></script>
</head>
<body class="container"> <h2>lh Test</h2>
<p>
Make the dual listbox be dual tree.
</p>
<div>
<form id="lhdemoform" action="#" method="post">
<select multiple="multiple" size="10" name="duallistbox_lhdemo">
<!--<option value="option1">Option 1</option>-->
</select>
<br>
<input type="button" value="初始数据" id="btnAddNew" />
<input type="button" value="获取选中数据" id="btnAddNew1" onclick="alert($('select[name=duallistbox_lhdemo]').bootstrapDualTree('getSelValues',false).join(' '))"/>
<input type="button" value="获取选中叶子节点数据" id="btnAddNew2" onclick="alert($('select[name=duallistbox_lhdemo]').bootstrapDualTree('getSelValues').join(' '))" />
<select multiple="multiple" size="10" name="duallistbox_lhdemo2">
<!--<option value="option1">Option 1</option>-->
</select>
<input type="button" value="获取选中数据" id="btnAddNew3" onclick="alert($('select[name=duallistbox_lhdemo2]').bootstrapDualTree('getSelValues',false).join(' '))" />
<input type="button" value="获取选中叶子节点数据" id="btnAddNew4" onclick="alert($('select[name=duallistbox_lhdemo2]').bootstrapDualTree('getSelValues').join(' '))" />
</form>
<script> //调用示例
var data = {
text: "t1",
value: "v1",
pid: "0",
children: [
{
text: "t11",
value: "v11",
pid: "v1",
children: [
{
text: "t111",
value: "v111",
pid: "v11",
},
{
text: "t112",
value: "v112",
pid: "v11",
children: [
{
text: "t1121",
value: "v1121",
pid: "v112",
},
{
text: "t1122",
value: "v1122",
pid: "v112",
},
],
},
]
},
{
text: "t12",
value: "v12",
pid: "v1",
children: [
{
text: "t121",
value: "v121",
pid: "v12",
},
{
text: "t122",
value: "v122",
pid: "v12",
},
]
},
],
}; var lhdemo = $('select[name="duallistbox_lhdemo"]').bootstrapDualTree({
nonSelectedListLabel: '未选',
selectedListLabel: '已选',
preserveSelectionOnMove: 'moved',
moveOnSelect: true,
//dualTree在dualListbox基础上新增的属性
data: data,//树形节点数据
selValues: ["v1121"], //默认选中节点值,为数组.如果不传,则默认不选中
indentSymbol: "-" //缩进符号,默认为-
});
var lhdemo2 = $('select[name="duallistbox_lhdemo2"]').bootstrapDualTree({
nonSelectedListLabel: '未选',
selectedListLabel: '已选',
preserveSelectionOnMove: 'moved',
moveOnSelect: true,
//dualTree在dualListbox基础上新增的属性
data: data,//树形节点数据
selValues: ["v1121", "v1122"], //默认选中节点值,为数组.如果不传,则默认不选中
indentSymbol: "-" //缩进符号,默认为-
});
$("#btnAddNew").click(function () {
//lhdemo.bootstrapDualTree("loadData", data, ["v1121", "v1122"]);//加载数据方法,可同时传递当前选中值
lhdemo.bootstrapDualTree("setValues", ["v1121", "v1122"]);//设置当前选中值
}); </script>
</div>
</body>

效果如下:

打包的bootstrap-dualtree.js文件代码如下:

 /**
* bootstrapDualTree extended from bootstrapDualListbox
* author: lh 2015-12-10
*/
(function ($, window, document, undefined) {
var pluginName = "bootstrapDualTree";//插件名称
//扩展jquery方法
$.fn[pluginName] = function (options) {
var returns;
var args = arguments;
if (options === undefined || typeof options === 'object') {
return this.each(function () {
if (!$.data(this, "plugin_" + pluginName)) {
$.data(this, "plugin_" + pluginName, new BootstrapDualTree(this, options));
}
});
} else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
this.each(function () {
var instance = $.data(this, 'plugin_' + pluginName);
// Tests that there's already a plugin-instance and checks that the requested public method exists
if (instance instanceof BootstrapDualTree && typeof instance[options] === 'function') {
// Call the method of our plugin instance, and pass it the supplied arguments.
returns = instance[options].apply(instance, Array.prototype.slice.call(args, 1));
}
});
};
return returns !== undefined ? returns : this;
}
//定义DualTree对象
function BootstrapDualTree(element, options) { var $e = $(element).bootstrapDualListbox(options);
this.tElement = $e; if (options.data) {
this.data = options.data;
}
if (options.indentSymbol!==undefined) {
this.setting.indentSymbol = options.indentSymbol;
}
if (options.selValues) {
this.selValues = options.selValues;
}
this.init();
var dualTree = this;
//bootstrap dual-listbox 在其发生变化的时候,触发change事件,实现双树都在这个事件中处理
$e.change(function () {
dualTree.refresh();
});
}
//定义可对外提供的方法
BootstrapDualTree.prototype = {
tElement:{},//select元素
data :{},//数据
selValues:[],//选择的节点值
setting:{
indentSymbol: "-",
},
lastOptions :[],//用于记录上一次的下列列表状态,以便通过比较识别移动操作的目标节点有哪些
loadData: function (dataL, selValuesL) {
data = dataL;
selValues = selValuesL || [];
this.init();
},
setValues: function (selValuesL) {
selValues = selValuesL || [];
this.init();
},
getSelValues: function (onlyLeaf) {
if (typeof(onlyLeaf)== "undefined") onlyLeaf = true;
var selValues1 = getSelValues(this.tElement,this.data, onlyLeaf);
return selValues1;
},
init: function () {
//alert(tElement)
this.tElement.find("option").remove();
showData(this.tElement, this.data, this.indentSymbol, 0, this.selValues);
recLastOptions(this.tElement, this);
this.tElement.bootstrapDualListbox("refresh");
if (this.selValues.length > 0) {
updateTreeSelectedStatus(this.tElement,this,this.data, this.selValues);
}
},
refresh: function () {
updateTreeSelectedStatus(this.tElement,this, this.data);
} }; //获取变化事件的方向:向右选择,向左选择
function getChangedDir() {
var dir = "all";
var srcHtml = event.srcElement.outerHTML;
//arrow-right关键字针对点击箭头移动的情形,nonselected-list针对选中时直接移动的情形
if (/arrow-right/.test(srcHtml) || /nonselected-list/.test(srcHtml)) {
dir = "right";
}
else if (/arrow-left/.test(srcHtml) || /selected-list/.test(srcHtml)) {
dir = "left";
}
return dir;
}
//记录上一个所有选项状态
function recLastOptions(tElement,tTree) {
tTree.lastOptions = [];
tElement.find("option").each(function () {
var curNode = $(this);
tTree.lastOptions.push({ value: curNode.attr("value"), selected: curNode.prop("selected") });
});
}
//获取发生变化的节点ID列表
function getChangedIds(tElement, lastOptions, dir) {
var changedIds = [];
if (dir == "right") {//向右,则取新选择的节点
newOptions = tElement.find("option");
for (var i = 0; i < newOptions.length; i++) {
if (newOptions[i].selected && !lastOptions[i].selected)
changedIds.push(lastOptions[i].value)
}
}
else if (dir == "left")//向左,则取新取消的节点
{
newOptions = tElement.find("option");
for (var i = 0; i < newOptions.length; i++) {
if (!newOptions[i].selected && lastOptions[i].selected)
changedIds.push(lastOptions[i].value)
}
}
return changedIds;
} //更新节点选中状态,将选中节点的父节点也都选中;
function updateTreeSelectedStatus(tElement, tTree, data, selValues) {
var dir = selValues && selValues.length > 0 ? "right" : getChangedDir();
var cIds = selValues || getChangedIds(tElement, tTree.lastOptions, dir);
console.log("changed:" + cIds)
if (dir == "right") {
//将所选节点的子节点及其路径上的节点也选中
for (var i = 0; i < cIds.length; i++) {
var node = findNodeById(data, cIds[i]);
console.log("handling-right:")
console.log(node)
selAllChildNodes(tElement, node);
selAcesterNodesInPath(tElement,data, node);
}
}
else if (dir == "left") {
//将所选节点的子节点也都取消选中
for (var i = 0; i < cIds.length; i++) {
var node = findNodeById(data, cIds[i]);
console.log("handling-left:")
console.log(node)
unSelAllChildNodes(tElement, node);
unSelAcesterNodesInPath(tElement,data, node);
}
} //重新添加未选节点及其父节点
//1、记录未选节点及其父节点
var nonSelNodes = [];
tElement.find("option").not(":selected").each(function () {
var curNode = $(this);
nonSelNodes.push(curNode.attr("value"));
while (curNode.length > 0) {
var pOption = tElement.find("option[value='" + curNode.attr("rel") + "']");
if (pOption.length > 0 && nonSelNodes.indexOf(pOption.attr("value")) < 0) nonSelNodes.push(pOption.attr("value"));
curNode = pOption;
}
});
//2、清除未选择的节点
tElement.find("option").not(':selected').remove();
console.log("nonSelNodes:" + nonSelNodes)
//3、重新显示左侧下拉列表
showNonSelData(tElement, data, tTree.setting.indentSymbol, 0, nonSelNodes); //重新显示已选择节点,以保持排序
var selNodes = [];
makeNoDuplicateSelNode(tElement);
var selOptions = tElement.find("option:selected");
for (var n = 0; n < selOptions.length; n++)
selNodes.push(selOptions[n].value);
selOptions.remove();
console.log("selNodes:" + selNodes)
showSelData(tElement, data, tTree.setting.indentSymbol, 0, selNodes); tElement.bootstrapDualListbox("refresh");
//记录新的下拉框状态
recLastOptions(tElement, tTree);
}
//递归显示所有节点
function showData(tElement, node,indentSymbol, depth, selValues) {
var selValues = selValues || [];
var withdraw = "";
for (var i = 0; i < depth; i++)
withdraw += indentSymbol;
tElement.append("<option value='" + node.value + "' rel='" + node.pid + "' " + (selValues.indexOf(node.value) >= 0 ? "selected" : "") + ">" + withdraw + node.text + "</option>");
if (node.children) {
for (var n = 0; n < node.children.length; n++) {
showData(tElement, node.children[n],indentSymbol, depth + 1, selValues);
}
}
}
//递归显示未选择节点
function showNonSelData(tElement, node, indentSymbol, depth, nonSelNodes) {
var withdraw = "";
for (var i = 0; i < depth; i++)
withdraw += indentSymbol;
if (nonSelNodes.indexOf(node.value) >= 0 && tElement.find("option[value='" + node.value + "']").not(":selected").length == 0) {
tElement.append("<option value='" + node.value + "' rel='" + node.pid + "'>" + withdraw + node.text + "</option>");
if (node.children) {
for (var n = 0; n < node.children.length; n++) {
showNonSelData(tElement, node.children[n],indentSymbol, depth + 1, nonSelNodes);
}
}
}
}
//递归显示已选择节点
function showSelData(tElement, node, indentSymbol, depth, selNodes) {
var withdraw = "";
for (var i = 0; i < depth; i++)
withdraw += indentSymbol;
if (selNodes.indexOf(node.value) >= 0 && tElement.find("option[value='" + node.value + "']:selected").length == 0) {
tElement.append("<option value='" + node.value + "' rel='" + node.pid + "' selected>" + withdraw + node.text + "</option>");
if (node.children) {
for (var n = 0; n < node.children.length; n++) {
showSelData(tElement, node.children[n], indentSymbol,depth + 1, selNodes);
}
}
}
}
//去掉已选择的重复节点
function makeNoDuplicateSelNode(tElement) {
tElement.find("option:selected").each(function () {
var curNode = $(this);
var options = tElement.find("option[value='" + curNode.attr("value") + "']:selected");
if (options.length > 1) {
for (var i = options.length; i > 0; i--)
$(options[i]).remove();
}
});
}
//如果一个节点选择了,则选中其子节点
function selAllChildNodes(tElement, node) {
if (node.children) {
for (var n = 0; n < node.children.length; n++) {
tElement.find("option[value='" + node.children[n].value + "']").prop("selected", true);
selAllChildNodes(tElement, node.children[n]);
}
}
}
//如果一个节点取消选择了,则取消选中其子节点
function unSelAllChildNodes(tElement, node) {
if (node.children) {
for (var n = 0; n < node.children.length; n++) {
tElement.find("option[value='" + node.children[n].value + "']").prop("selected", false);
unSelAllChildNodes(tElement, node.children[n]);
}
}
}
//获取选中的值列表
function getSelValues(tElement, node, onlyLeaf) {
var selValuesTmp = [];
tElement.find("option[value='" + node.value + "']").each(function () {
if ($(this).prop("selected")) {
if (!node.children || node.children.length == 0 || !onlyLeaf) {
selValuesTmp.push(node.value);
}
if (node.children) {
for (var n = 0; n < node.children.length; n++) {
selValuesTmp = selValuesTmp.concat(getSelValues(tElement,node.children[n], onlyLeaf));
}
}
}
});
return selValuesTmp;
}
//选中一个节点的路径上的祖先节点
function selAcesterNodesInPath(tElement,root, node) {
var curNode = node;
while (curNode.pid != "0") {
curNode = findNodeById(root, curNode.pid);
var pOption = tElement.find("option[value='" + curNode.value + "']");
if (pOption.length > 0) pOption.prop("selected", true);
}
}
//取消一个节点的路径上的祖先节点,这些节点没有子节点被选中
function unSelAcesterNodesInPath(tElement, root, node) {
var curNode = node;
while (curNode.pid != "0") {
curNode = findNodeById(root, curNode.pid);
if (!hasSelChildrenNodes(tElement, curNode)) {
var pOption = tElement.find("option[value='" + curNode.value + "']");
if (pOption.length > 0) pOption.prop("selected", false);
}
}
}
//从树中寻找某个id的节点
function findNodeById(node, id) {
if (node.value == id) {
return node;
}
else {
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
var rsNode = findNodeById(node.children[i], id);
if (rsNode != null) return rsNode;
}
}
}
return null;
}
//判断某个节点的子节点是否被选中
function hasSelChildrenNodes(tElement, node) {
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
var pOption = tElement.find("option[value='" + node.children[i].value + "']:selected");
if (pOption.length > 0) return true;
}
}
return false;
}
})(jQuery, window, document);

BootStrap-DualListBox怎样改造成为双树的更多相关文章

  1. [luogu3359]改造异或树

    [luogu3359]改造异或树 luogu 和之前某道题类似只有删边的话考虑倒着加边 但是怎么统计答案呢? 我们考虑以任意点为根dfs一遍求出每个点到根的路径异或和s[i] 这样任意两点x,y的路径 ...

  2. bootstrap的导航改造

    在使用bootstrap制作后台时用到了响应式导航条,其中dropdown组件更是用的比较多,用的多需要点击的就多,dropdown默认鼠标左键单击才展开,如果使用鼠标放上去(hover)就展开则会省 ...

  3. bootstrap日期控件(双日期、清空等问题解决)

    bootstrap以它优美的外观和丰富的组件,使它成为目前最流行的前端框架.在项目开发中,我们使用它的日期控件确实遇到了一些问题: 1.日期控件后面两个图标点击触发失效 2.双日期关联问题 3.双日期 ...

  4. luoguP3359 改造异或树 线段树合并

    删边转化为加边 然后每次用线段树合并就行..... 确确实实很简单 然而为什么线段树合并跑不过$splay$的启发式合并,常数稍大了点... 复杂度$O(n \log n)$ #include < ...

  5. 洛谷 P3359 改造异或树

    题目描述 给定一棵n 个点的树,每条边上都有一个权值.现在按顺序删掉所有的n-1条边,每删掉一条边询问当前有多少条路径满足路径上所有边权值异或和为0. 输入输出格式 输入格式: 第一行一个整数n. 接 ...

  6. luoguP3359 改造异或树

    https://www.luogu.org/problemnew/show/P3359 因为 a ^ b ^ b = a,所以我们预处理 1 到所有点的距离,将删边的操作反过来变成加边,对于每一个联通 ...

  7. bootstrap treeview实现菜单树

    本博客,介绍通过Bootstrap的treeview插件实现菜单树的功能. treeview链接:http://www.htmleaf.com/Demo/201502141380.html ORM框架 ...

  8. 基于bootstrap的jQuery多级列表树插件 treeview

    http://www.cnblogs.com/mfc-itblog/p/5233453.html http://www.htmleaf.com/jQuery/Menu-Navigation/20150 ...

  9. 基于bootstrap的jQuery多级列表树插件

    简要教程 bootstrap-treeview是一款效果非常酷的基于bootstrap的jQuery多级列表树插件.该jQuery插件基于Twitter Bootstrap,以简单和优雅的方式来显示一 ...

随机推荐

  1. 机器学习实战之kNN算法

    机器学习实战这本书是基于python的,如果我们想要完成python开发,那么python的开发环境必不可少: (1)python3.52,64位,这是我用的python版本 (2)numpy 1.1 ...

  2. Ubuntu 终端命令整理

    一.文件目录类 1.建立目录:mkdir 目录名 2.删除空目录:rmdir 目录名 3.无条件删除子目录: rm -rf 目录名 4.改变当前目录:cd 目录名 (进入用户home目录:cd ~:进 ...

  3. POJ 3693 Maximum repetition substring ——后缀数组

    重复次数最多的字串,我们可以枚举循环节的长度. 然后正反两次LCP,然后发现如果长度%L有剩余的情况时,答案是在一个区间内的. 所以需要找到区间内最小的rk值. 两个后缀数组,四个ST表,$\Thet ...

  4. android源码mk文件里的TARGET_OUT指向哪里?

    android源码核心变量大都在build/core/envsetup.mk中建立 在该文件中,可以找到 TARGET_OUT := $(PRODUCT_OUT)/$(TARGET_COPY_OUT_ ...

  5. hdu 5040 Instrusive【BFS+优先队列】

    11733274 2014-09-26 12:42:31 Accepted 5040 62MS 1592K 4848 B G++ czy 先转一个优先队列的用法: http://www.cppblog ...

  6. spring-boot-nginx代理-docker-compose部署

    在本地测试,使用docker部署不用在意环境 java测试项目: web框架:spring boot 框架 项目管理:maven 数据库:redis + postgres + mongo 部署相关:n ...

  7. Java 并发编程中的 CountDownLatch 锁用于多个线程同时开始运行或主线程等待子线程结束

    Java 5 开始引入的 Concurrent 并发软件包里面的 CountDownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是 ...

  8. Js 流程控制

    流程控制 顺序.分支.循环 顺序结构 代码一行一行从上往下执行并解析 分支结构 if语句 switch语句 if语句 单分支 if(条件表达式){ //语句块 } 含义:当条件表达式为真的时候就执行里 ...

  9. Docker如何部署Python项目

    Docker 部署Python项目 作者:白宁超 2019年5月24日09:09:00 导读: 软件开发最大的麻烦事之一就是环境配置,操作系统设置,各种库和组件的安装.只有它们都正确,软件才能运行.如 ...

  10. CPU 内存 硬盘的区别

    第一点:CPU 是处理器,内存和硬盘是存储器,受CPU 的控制.  第二点:由于内存的速度很快,在电脑运行的过程中,CPU通常只与内存交换数据,但内存断电数据就会全部丢失,因此电脑使用硬盘作为主要的存 ...