项目开发中遇到一颗树(单选、多选);
github L6zt
项目中遇到这个功能,与其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');