javascript 单选、复选树 —

javascript/jquery

浏览数:138

2019-3-31

AD:资源代下载服务

项目开发中遇到一颗树(单选、多选);

项目中遇到这个功能,与其copy一个不如自己造个轮子。预览地址
设计主要思路:
1.展现层树的显示 用递归函数罗列出页面显示效果。
2.不同的功能对应不用模式model来区分、对应这不同的展示效果(ui交互效果、初始化数据选择形式)
所以采用对应不同的模式分别写出(不同初始渲染状态函数和ui效果交互函数)。感觉这是这样处理比较合适。
3.插件里面维护一个获取当前选中状态的函数返回{id, name} 为单元的数组。
4.id 查询路径函数(findIdDir),所有初始化页面状态逻辑都基于它

主要函数代码:

// 搜寻回现id中具体的位置,为了定位数据点。为后面操纵数据状态(例如是不是选中状态)提供方便。
假如findIdDir返回数据 1-2 即该节点在 [{},{childs: [{},{id: ‘xxx’, name: ‘我就是1-2节点’}]},{}]

  const findIdDir = (data, id, dir) => {
    dir = dir ? dir : '';
    /*--||--||--||--*/
    for (let idx = 0, lg = data.length; idx < lg; idx++) {
      if (data[idx].id == id) {
        return dir === '' ? `${idx}` : `${dir}-${idx}`
      }
      if (data[idx].childs) {
        let curDir = `${dir ?  `${dir}-` : ''}${idx}`;
        let result = findIdDir(data[idx].childs, id, curDir);
        if (result) {
          return result
        }
      }
    }
    return undefined
  };

// js 代码

/*
 *  model ---- 0 1 2模式 
 * 0 是单选模式  
 * 1 是回连模式(选择父节点子节联动选中取消,子节点选中联调父节点是否选中) 多选模式 
 * 2 特殊 不回联模式
 *  onchange ----> 选中触发回调
 *  idList: [] -----> 选中节点结合
 */
console.log('init ---- statrt');
(function() {
  const pluginName = 'jctree';
  const noop = function() {};
  // 路径函数
  const findIdDir = (data, id, dir) => {
    dir = dir ? dir : '';
    /*--||--||--||--*/
    for (let idx = 0, lg = data.length; idx < lg; idx++) {
      if (data[idx].id == id) {
        return dir === '' ? `${idx}` : `${dir}-${idx}`
      }
      if (data[idx].childs) {
        let curDir = `${dir ?  `${dir}-` : ''}${idx}`;
        let result = findIdDir(data[idx].childs, id, curDir);
        if (result) {
          return result
        }
      }
    }
    return undefined
  };
  const defaultOption = {
    model: 0,
    data: [],
    onchange: noop,
    idList: []
  }
  
  function JcTree(options) {
    this._options = defaultOption;
    if (!options.data || !Array.isArray(options.data)) {
      console.warn('树需要初始化数据data且data应为数组')
    };
    options.data = $.extend(true, [], options.data);
    $.extend(this._options, options || {});
    this.init();
  };
  // 渲染树
  JcTree.prototype.renderTree = function(data, open, index, dir) {
    index = undefined === index ? 0 : index;
    dir = undefined === dir ? '' : dir;
    const nextIndex = index + 1;
    const className = open ? '' : 'hide';
    let htmlStr = `<div class="tree-fh-node ${className}">`;
    // data icon-check text-icon del-btn check.svg
    data && data.forEach((d, idx) => {
      let {
        id,
        name,
        childs,
        open,
        leafFlag,
        checked,
        hasChildSelect
      } = d;
      let curDir = dir === '' ? `${idx}` : `${dir}-${idx}`;
      let showToggleBtnFlag = leafFlag;
      htmlStr += `<div class="tree-fh-item" data-id="${id}" data-index="${index}" data-dir="${curDir}">` +
        (showToggleBtnFlag && childs && childs.length ? `<span class="tree-handle-toggle" data-id="${id}" data-index="${index}" data-dir="${curDir}">${open ? '-' : '+'}</span>` : '<span class="tree-handle-toggle-none"></span>') +
        `<label class="checbox-container"><span data-id="${id}" data-dir="${curDir}" data-name="${name}" class="checkbox ${checked? 'active' : ''}"><i class="icon-check text-icon"></i></span></label>` +
        `<span class="tree-node-name">${name}</span>` +
        `<span class="has-child-select ${hasChildSelect ? '' : 'hide'} ?>">(下级有选中节点)</span>` +
        `</div>`;
      if (childs && childs.length > 0) {
        htmlStr += this.renderTree(childs, open, nextIndex, curDir);
      }
    });
    return htmlStr += `</div>`
  };
  // 初始化数据
  JcTree.prototype.initSingleData = function() {
    const {
      _options: {
        data,
        idList
      }
    } = this;
    if (idList.length === 0) {
      return
    };
    const dirList = idList.map(id => findIdDir(data, id));
    dirList.forEach(dir => {
      if (dir === undefined) return;
      const indexList = dir.split('-').filter(i => i !== '');
      let lg = indexList.length;
      let item = data;
      for (let i = 0; i < lg; i++) {
        let curIndex = indexList[i];
        if (i === lg - 1) {
          item[curIndex].checked = true
        } else {
          if (lg !== 1) {
            item[curIndex].open = true
          }
        }
        item = item[curIndex].childs
      }
    });
  };
  JcTree.prototype.initMulitData = function() {
    const {
      _options: {
        data,
        idList
      }
    } = this;
    const syncChildState = function(data) {
      if (data.childs) {
        data.childs.forEach(syncChildState);
      }
      data.open = true;
      data.checked = true;
    };
    if (idList.length === 0) {
      return
    };
    // 对id 路径进行排序·规则 --- 例如当 存在 ‘1-2-1’ 和 ‘1-2’ 两个对应的路径,
    // 此时表示 ‘1-2’ 以下的点都为选中的状态,此时 再出现 ‘1-2-2’,就不用关注这个点的状态,
    // 因为这个是在 ‘1-2’ 以下的所以是选中状态。
    let originDirList = idList.map(id => findIdDir(data, id)).filter(d => d !== undefined).sort();
    let dirList = [];
    // 打牌比较 如果 如果前面相同的话 拿出来
    while (originDirList.length) {
      let cur = originDirList.shift();
      dirList.push(cur)
      for (var i = 0; i < originDirList.length;) {
        if (originDirList[i].indexOf(cur) === 0) {
          originDirList.splice(i, 1)
        } else {
          i++
        }
      }
    };
    // 初始化父子节点 /0/
    let curItems = [];
    // 排序优化
    dirList.forEach(dir => {
      if (dir === undefined) return;
      const indexList = dir.split('-').filter(i => i !== '');
      let lg = indexList.length;
      let item = data;
      for (let i = 0; i < lg; i++) {
        let curIndex = indexList[i];
        if (i === lg - 1) {
          item[curIndex].checked = true;
          curItems.push(item[curIndex]);
        } else {
          if (lg !== 1) {
            item[curIndex].open = true
            item[curIndex].hasChildSelect = true
          }
        }
        item = item[curIndex].childs
      }
    });
    curItems.forEach(syncChildState);
  };
  JcTree.prototype.initMulitSpData = function() {
    const {
      _options: {
        data,
        idList
      }
    } = this;
    if (idList.length === 0) {
      return
    };
    // 打牌比较 如果 如果前面相同的话 拿出来
    let dirList = idList.map(id => findIdDir(data, id)).filter(d => d !== undefined).sort();
    // 初始化父子节点 /0/
    let curItems = [];
    // 排序优化
    dirList.forEach(dir => {
      if (dir === undefined) return;
      const indexList = dir.split('-').filter(i => i !== '');
      let lg = indexList.length;
      let item = data;
      for (let i = 0; i < lg; i++) {
        let curIndex = indexList[i];
        if (i === lg - 1) {
          item[curIndex].checked = true;
          curItems.push(item[curIndex]);
        } else {
          if (lg !== 1) {
            item[curIndex].open = true;
            item[curIndex].hasChildSelect = true
          }
        }
        item = item[curIndex].childs
      }
    });
  };
  JcTree.prototype.bindEventModelSingle = function() {
    const $root = this._options.$el;
    const _this = this;
    $root.on(`click.${pluginName}`, '.tree-handle-toggle', function() {
      let toggleText;
      const $curEl = $(this);
      const $parentNext = $curEl.parent('.tree-fh-item').next();
      $parentNext.toggleClass('hide');
      toggleText = $parentNext.hasClass('hide') ? '+' : '-';
      $curEl.text(toggleText);
    }).on(`click.${pluginName}`, 'span.checkbox', function() {
      const $el = $(this);
      $el.toggleClass('active');
      const id = $el.data('id');
      let selectFlag = $el.hasClass('active');
      if (selectFlag) {
        $root.find('span.checkbox').removeClass('active')
        $el.addClass('active')
        _this._options.onchange(id);
      } else {
        $el.removeClass('active')
      }
    })
  };
  JcTree.prototype.bindEventModelMulit = function() {
    const $root = this._options.$el;
    const data = this._options.data;
    const _this = this;
    $root.on(`click.${pluginName}`, '.tree-handle-toggle', function() {
      let toggleText;
      const $curEl = $(this);
      const $parentNext = $curEl.parent('.tree-fh-item').next();
      $parentNext.toggleClass('hide');
      toggleText = $parentNext.hasClass('hide') ? '+' : '-';
      $curEl.text(toggleText);
    }).on(`click.${pluginName}`, 'span.checkbox', function() {
      const $el = $(this);
      $el.toggleClass('active');
      const dir = $(this).data('dir').toString();
      const dirIndex = dir.split('-');
      let parentsDirs = [];
      let parentDir = '';
      const checkFlag = $el.hasClass('active');
      const $parent = $el.closest('.tree-fh-item');
      // 父级 对 下级效果
      const $childsParents = $parent.next('.tree-fh-node');
      checkFlag ? $childsParents.find('span.checkbox').addClass('active') : $childsParents.find('span.checkbox').removeClass('active')
      // 寻根节点
      dirIndex.forEach(d => {
        parentDir = parentDir === '' ? d : `${parentDir}-${d}`
        parentsDirs.push(parentDir)
      });
      // 找相应的父节点
      parentsDirs = parentsDirs.map(dir => `.tree-fh-item[data-dir="${dir}"]`).reverse();
      parentsDirs.shift();
      parentsDirs.forEach(function(selector) {
        const $el = $(selector, $root);
        const $next = $el.next();
        const findAllCheckboxs = $('span.checkbox', $next);
        let flag = true;
        findAllCheckboxs.each(function() {
          if (!$(this).hasClass('active')) {
            flag = false
            return false
          }
        });
        flag ? $el.find('span.checkbox').addClass('active') : $el.find('span.checkbox').removeClass('active');
      })
      _this._options.onchange(_this.getIdList());
    })
  };
  JcTree.prototype.bindEventModelMulitSp = function() {
    const $root = this._options.$el;
    const data = this._options.data;
    const _this = this;
    $root.on(`click.${pluginName}`, '.tree-handle-toggle', function() {
      let toggleText;
      const $curEl = $(this);
      const $parentNext = $curEl.parent('.tree-fh-item').next();
      $parentNext.toggleClass('hide');
      toggleText = $parentNext.hasClass('hide') ? '+' : '-';
      $curEl.text(toggleText);
    }).on(`click.${pluginName}`, 'span.checkbox', function() {
      const $el = $(this);
      $el.toggleClass('active');
      const dir = $(this).data('dir').toString();
      const dirIndex = dir.split('-');
      let parentsDirs = [];
      let parentDir = '';
      const checkFlag = $el.hasClass('active');
      const $parent = $el.closest('.tree-fh-item');
      // 父级 对 下级效果
      // 寻根节点
      dirIndex.forEach(d => {
        parentDir = parentDir === '' ? d : `${parentDir}-${d}`
        parentsDirs.push(parentDir)
      });
      // 找相应的父节点
      parentsDirs = parentsDirs.map(dir => `.tree-fh-item[data-dir="${dir}"]`);
      parentsDirs.pop();
      parentsDirs.forEach(function(selector) {
        const $el = $(selector, $root);
        const $hasChildSelect = $el.find('.has-child-select');
        const $next = $el.next();
        const findAllCheckboxs = $('span.checkbox', $next);
        let flag = false;
        findAllCheckboxs.each(function() {
          if ($(this).hasClass('active')) {
            flag = true
            return false
          }
        });
        !flag ? $hasChildSelect.addClass('hide') : $hasChildSelect.removeClass('hide')
      })
      _this._options.onchange(_this.getIdList());
    })
  }
  //
  JcTree.prototype.getIdList = function() {
    const $root = this._options.$el;
    return $('span.active', $root).filter('.active').map((index, el) => {
      const $el = $(el);
      return {
        id: $el.data('id'),
        name: $el.data('name')
      }
    }).get();
  };
  // 初始化树
  JcTree.prototype.init = function() {
    switch (this._options.model) {
      case 0:
      {
        this.initSingleData();
        break;
      }
      case 1:
      {
        this.initMulitData();
        break;
      }
      case 2:
      {
        this.initMulitSpData();
        break;
      }
    }
    let result = this.renderTree(this._options.data, true);
    result = `<div class="tree-root-warp">${result} </div>`
    this._options.$el.html(result);
    switch (this._options.model) {
      case 0:
      {
        this.bindEventModelSingle();
        break;
      }
      case 1:
      {
        this.bindEventModelMulit();
        break;
      }
      case 2:
      {
        this.bindEventModelMulitSp();
        break;
      }
    }
  };
  
  $.fn.JcTree = function(options) {
    const $el = this;
    options = Object.assign({}, options, {
      $el
    });
    const jctree = new JcTree(options);
    const data = $el.data('jctree');
    if (data) this.off(`click.${pluginName}`);
    $el.data('jctree', jctree);
    return this
  }
})();
/**************************模拟树数据********************************/
// 后端数据
const ajaxData =  {
  "flag":1,
  "data":{"root":{"id":1,"level":0,"data":{"id":1,"level":0,"parentId":0,"name":"全部","categoryId":1,"leafFlag":1},"parentId":0,"childs":[{"id":2,"level":1,"data":{"id":2,"level":1,"parentId":1,"name":"导入默认分类","categoryId":2,"leafFlag":1},"parentId":1,"childs":[]},{"id":3,"level":1,"data":{"id":3,"level":1,"parentId":1,"name":"测试1级","categoryId":3,"leafFlag":0},"parentId":1,"childs":[{"id":5,"level":2,"data":{"id":5,"level":2,"parentId":3,"name":"测试2级","categoryId":5,"leafFlag":0},"parentId":3,"childs":[{"id":7,"level":3,"data":{"id":7,"level":3,"parentId":5,"name":"测试3级","categoryId":7,"leafFlag":1},"parentId":5,"childs":[]},{"id":8,"level":3,"data":{"id":8,"level":3,"parentId":5,"name":"测试3级b","categoryId":8,"leafFlag":1},"parentId":5,"childs":[]}]},{"id":6,"level":2,"data":{"id":6,"level":2,"parentId":3,"name":"测试2级b","categoryId":6,"leafFlag":0},"parentId":3,"childs":[]}]},{"id":4,"level":1,"data":{"id":4,"level":1,"parentId":1,"name":"测试1级b","categoryId":4,"leafFlag":0},"parentId":1,"childs":[]}]},"rootFlag":-1,"count":8}
};
let data = [ajaxData.data.root];
const transData = function (data, resultOut) {
  resultOut = resultOut ? resultOut : [];
  data.forEach((d, index) => {
    let leafMsg = {};
    leafMsg.id = d.id;
    leafMsg.childs = d.childs;
    leafMsg.name = d.data.name;
    leafMsg.leafFlag = d.data.leafFlag;
    leafMsg.level = d.level;
    if (d.childs) {
      const childs = leafMsg.childs = [];
      transData(d.childs, childs);
    }
    resultOut.push(leafMsg);
  });
  return resultOut
};
data = transData(data);
data = [{
  "id": 1,
  "childs": [{
    "id": 2,
    "childs": [{
      id: 9,
      name: '测试',
      level: 0
    }],
    "name": "导入默认分类",
    "leafFlag": 1,
    "level": 1
  }, {
    "id": 3,
    "childs": [{
      "id": 5,
      "childs": [{
        "id": 7,
        "childs": [],
        "name": "测试3级",
        "leafFlag": 0,
        "level": 3
      }, {
        "id": 8,
        "childs": [],
        "name": "测试3级b",
        "leafFlag": 0,
        "level": 3
      }],
      "name": "测试2级",
      "leafFlag": 1,
      "level": 2
    }, {
      "id": 6,
      "childs": [],
      "name": "测试2级b",
      "leafFlag": 1,
      "level": 2
    }],
    "name": "测试1级",
    "leafFlag": 1,
    "level": 1
  }, {
    "id": 4,
    "childs": [{
      id: 13,
      name: '走吧',
      leafFlag: 1,
      childs: [{
        id: 113,
        name: '走吧',
        leafFlag: 1
      }, {
        id: 114,
        name: '哈哈',
        leafFlag: 1
      }, {
        id: 213,
        name: 'jc',
        leafFlag: 1,
        childs : [
          {
            id: 313,
            name: '走吧',
            leafFlag: 1
          }, {
            id: 314,
            name: '哈哈',
            leafFlag: 1
          }, {
            id: 413,
            name: 'jc',
            leafFlag: 1
          }, {
            id: 919,
            name: 'xx',
            leafFlag: 1,
            childs: [
              {
                id: 818,
                name: '结束',
                leafFlag: 0,
              }
            ]
          }
        ]
      }]
    }, {
      id: 414,
      name: '哈哈',
      leafFlag: 1
    }, {
      id: 23,
      name: 'jc',
      leafFlag: 1,
      childs : [
        {
          id: 33,
          name: '走吧',
          leafFlag: 1
        }, {
          id: 34,
          name: '哈哈',
          leafFlag: 1
        }, {
          id: 43,
          name: 'jc',
          leafFlag: 1
        }, {
          id: 99,
          name: 'xx',
          leafFlag: 1,
          childs: [
            {
              id: 88,
              name: '结束',
              leafFlag: 0,
            }
          ]
        }
      ]
    }],
    "name": "测试1级b",
    "leafFlag": 1,
    "level": 1
  }],
  "name": "全部",
  "leafFlag": 1,
  "level": 0
}];
data.forEach(d => {
  d.open = true;
});
$('#model-0').JcTree({
  data: data,
  model: 0,
  idList: [1]
})
$('#model-1').JcTree({
  data: data,
  model: 1,
  idList: [1]
})
$('#model-2').JcTree({
  data: data,
  model: 2,
  idList: [1]
})
console.log('init ---- end');