多级选择组件解决实践

Posted by 梧桐和风的博客 on September 14, 2017

问题描述

这里的多级选择组件问题,指的是存在一个多级的选择组件,当点击某个节点时,该节点及其下的所有节点都要选中,若该节点并列的所有兄弟节点都已选中,则其父节点也要勾选,依此到最顶端节点。反选也类似逻辑。

这个问题也符合平日的认知习惯。如下图所示:

这里写图片描述

如点新华区,则其下所有街道都要选中,再点击桥西区桥西区下的街道要选中,同时石家庄市这个节点要选中。若再点击廊坊市这个节点,则整个河北省都应该选中。

解决方法

有一个问题是,我们并不知道这个选择组件有多少级。如上示例为3级,但实际中是不确定的。

从问题来看,设置目标节点(点击的节点)及其下的节点的选择状态比较容易办到,如使用jquery查询其下的所有子节点即可。比较困难的是,怎样设置父节点的状态(是否选中)?

可以想象的是,查询兄弟节点的状态,若兄弟节点有没有选中的,任务就结束了(直接返回),若兄弟节点都选中,说明其父节点也应该需要选中,好,设置父节点选中,这样还要判断父节点的兄弟节点…….这样一直循环到顶端节点。这就是所谓的递归的套路。那有没有简单的方法呢?

利用冒泡的解决方法

有一个解决的方法。原理和上面提到的类似,不过向上递归的过程我们用冒泡实现。

这种方法有一定的限制条件,首先我们把每个节点都添加一个相同的样式(有同一个class),父节点需要包含子节点(即点击子节点时事件需能冒泡上传到父节点)。好了,这样我们只需处理一层父节点即可。

处理父节点时,若其兄弟节点有未选中的,阻止冒泡事件即可。

实践

如上面的示例,解决方法实现如下:

页面

<!DOCTYPE html>
<html>
<head>
    <title>多级选择组件</title>
    <style>
        .one-level {
            margin-left: 30px;
        }
    </style>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
</head>
<body>
    <div class="node one-level">
        <button type="">选择</button>
        <label><input type="checkbox" value="" />河北省</label>
        <div class="node one-level">
            <button type="">选择</button>

            <label><input type="checkbox" value="" />石家庄市</label>

            <div class="node one-level">
                <button type="">选择</button>

                <label><input type="checkbox"  />新华区</label>
                <div class="node one-level">
                    <button type="">选择</button>
                    <label><input type="checkbox"  />A街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />B街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>
                    <label><input type="checkbox"  />C街道</label>
                </div>
            </div>
            <div class="node one-level">
                <button type="">选择</button>

                <label><input type="checkbox"  />桥西区</label>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />1街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />2街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />3街道</label>
                </div>
            </div>
        </div>
        <div class="node one-level">
            <button type="">选择</button>

            <label><input type="checkbox"  />廊坊市</label>

            <div class="node one-level">
                <button type="">选择</button>

                <label><input type="checkbox"  />广阳区</label>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />A街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>
                    <label><input type="checkbox"  />B街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>
                    <label><input type="checkbox"  />C街道</label>
                </div>
            </div>
            <div class="node one-level">
                <button type="">选择</button>
                <label><input type="checkbox"  />开发区</label>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />A街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />B街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />C街道</label>
                </div>
            </div>
            <div class="node one-level">
                <button type="">选择</button>
                <label><input type="checkbox"  />安次区</label>
                <div class="node one-level">
                    <button type="">选择</button>
                    <label><input type="checkbox"  />A街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>

                    <label><input type="checkbox"  />B街道</label>
                </div>
                <div class="node one-level">
                    <button type="">选择</button>
                    <label><input type="checkbox"  />C街道</label>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

js核心实现

$('.node').on('click', function() {
            // 判断事件源,只响应按钮单击事件
            var tagName = event.target.tagName;
            if(tagName!=='BUTTON'){
                return false;
            }
            var name = $(this).children('label').text();
            console.log('name:' + name);
            var self = $(this).children('label').find('input')[0];
            var children = $(this).children('.node').find('input');
            if (self.checked) { //已选择,取消选择
                self.checked = false;
                var isAll = true;
                for (let i = 0; i < children.length; i++) {
                    if (!children[i].checked) {
                        isAll = false;
                        break;
                    }
                }
                if (isAll) {
                    for (let i = 0; i < children.length; i++) {
                        children[i].checked = false;
                    }
                }
            } else { //未选择,勾选
                self.checked = true;
                for (let i = 0; i < children.length; i++) {
                    children[i].checked = true;
                }
            }
            var sibs = $(this).siblings().children('label').find('input');
            var isfull = true;
            //查看兄弟节点,看其是否有未选中的
            for (var i = 0; i < sibs.length; i++) {
                if (!sibs[i].checked) {
                    isfull = false;
                    break;
                }
            }
            if (!isfull) {
                event.stopPropagation(); //  阻止事件冒泡
            }
});

说明:

  1. 这样做会使单击事件的区域变为块状(区域为div),所以需要在开头判断事件源。
  2. 对于取消选中,需要判断:若是子节点有未选中的,则不需要管子节点,只需把自身取消选中即可。
  3. 本文未对中间态(选中、未选中、半选中)做处理,有兴趣的你可以试试