|
@@ -25,6 +25,7 @@ var idTree = {
|
|
|
return null;
|
|
return null;
|
|
|
},
|
|
},
|
|
|
reSortNodes: function (nodes, recursive) {
|
|
reSortNodes: function (nodes, recursive) {
|
|
|
|
|
+
|
|
|
var temp = [], first;
|
|
var temp = [], first;
|
|
|
var findFirstNode = function (nodes) {
|
|
var findFirstNode = function (nodes) {
|
|
|
return tools.findNode(nodes, function (node) {
|
|
return tools.findNode(nodes, function (node) {
|
|
@@ -32,14 +33,24 @@ var idTree = {
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
var moveNode = function (node, orgArray, newArray, newIndex) {
|
|
var moveNode = function (node, orgArray, newArray, newIndex) {
|
|
|
- var next;
|
|
|
|
|
- orgArray.splice(orgArray.indexOf(node), 1);
|
|
|
|
|
- newArray.splice(newIndex, 0, node);
|
|
|
|
|
- if (node.getNextSiblingID() !== -1) {
|
|
|
|
|
- next = node.nextSibling;
|
|
|
|
|
- if (next && (orgArray.indexOf(next) >= 0)) {
|
|
|
|
|
- moveNode(next, orgArray, newArray, newIndex + 1);
|
|
|
|
|
|
|
+ var current = node, currentIndex, next;
|
|
|
|
|
+ // 不能递归:同级节点很多时,即使链正确也会超过浏览器调用栈。
|
|
|
|
|
+ while (current) {
|
|
|
|
|
+ currentIndex = orgArray.indexOf(current);
|
|
|
|
|
+ if (currentIndex < 0) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ orgArray.splice(currentIndex, 1);
|
|
|
|
|
+ newArray.splice(newIndex, 0, current);
|
|
|
|
|
+ newIndex++;
|
|
|
|
|
+
|
|
|
|
|
+ next = current.nextSibling;
|
|
|
|
|
+ // next 不在当前同级数组中,说明它已处理、跨层、缺失、自指或成环。
|
|
|
|
|
+ // 停止当前链,剩余节点交给外层 while 继续显示。
|
|
|
|
|
+ if (!next || orgArray.indexOf(next) < 0) {
|
|
|
|
|
+ break;
|
|
|
}
|
|
}
|
|
|
|
|
+ current = next;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
if (nodes.length === 0) {
|
|
if (nodes.length === 0) {
|
|
@@ -926,6 +937,323 @@ var idTree = {
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查原始树数据,并给出可用于修复数据的 updateDatas。
|
|
|
|
|
+ * 最好在 loadDatas 前调用 newCheck(datas),避免错误的关系影响树的构建。
|
|
|
|
|
+ * 不传 datas 时检查当前树中保存的原始 data;本方法不会修改原数据。
|
|
|
|
|
+ */
|
|
|
|
|
+ Tree.prototype.newCheck = function (datas) {
|
|
|
|
|
+ var that = this;
|
|
|
|
|
+ var idField = this.setting.id;
|
|
|
|
|
+ var pidField = this.setting.pid;
|
|
|
|
|
+ var nidField = this.setting.nid;
|
|
|
|
|
+ var rootId = this.setting.rootId;
|
|
|
|
|
+ var source = Array.isArray(datas) ? datas : Object.keys(this.nodes).map(function (key) {
|
|
|
|
|
+ return that.nodes[key].data;
|
|
|
|
|
+ });
|
|
|
|
|
+ var problems = [];
|
|
|
|
|
+ var updateMap = Object.create(null);
|
|
|
|
|
+ var nodeMap = Object.create(null);
|
|
|
|
|
+ var duplicateMap = Object.create(null);
|
|
|
|
|
+ var duplicates = [];
|
|
|
|
|
+ var stackRisks = [];
|
|
|
|
|
+ var records = [];
|
|
|
|
|
+
|
|
|
|
|
+ var valueKey = function (value) {
|
|
|
|
|
+ return String(value);
|
|
|
|
|
+ };
|
|
|
|
|
+ var sameValue = function (left, right) {
|
|
|
|
|
+ return left == right;
|
|
|
|
|
+ };
|
|
|
|
|
+ var addProblem = function (type, record, field, currentValue, suggestedValue, message) {
|
|
|
|
|
+ problems.push({
|
|
|
|
|
+ type: type,
|
|
|
|
|
+ id: record ? record.id : undefined,
|
|
|
|
|
+ dataIndex: record ? record.index : undefined,
|
|
|
|
|
+ rowNumber: record ? record.index + 1 : undefined,
|
|
|
|
|
+ // 保留对象引用供控制台展开,不能 JSON.stringify:Node 包含 tree 循环引用。
|
|
|
|
|
+ nodeData: record ? record.data : null,
|
|
|
|
|
+ field: field || '',
|
|
|
|
|
+ currentValue: currentValue,
|
|
|
|
|
+ suggestedValue: suggestedValue,
|
|
|
|
|
+ message: message
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+ var suggestUpdate = function (record, field, value) {
|
|
|
|
|
+ var key = valueKey(record.id);
|
|
|
|
|
+ if (!updateMap[key]) {
|
|
|
|
|
+ updateMap[key] = {};
|
|
|
|
|
+ updateMap[key][idField] = record.id;
|
|
|
|
|
+ }
|
|
|
|
|
+ updateMap[key][field] = value;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ source.forEach(function (data, index) {
|
|
|
|
|
+ // 兼容 newCheck(rawDatas) 和 newCheck(tree.items) 两种调用方式。
|
|
|
|
|
+ // Tree Node 中包含 node.tree.nodes -> node 的循环关系,只检查其原始 data。
|
|
|
|
|
+ var rawData = data && typeof data.getID === 'function' && data.data ? data.data : data;
|
|
|
|
|
+ var record = {
|
|
|
|
|
+ data: rawData,
|
|
|
|
|
+ id: rawData[idField],
|
|
|
|
|
+ pid: rawData[pidField],
|
|
|
|
|
+ nid: rawData[nidField],
|
|
|
|
|
+ index: index
|
|
|
|
|
+ };
|
|
|
|
|
+ var key = valueKey(record.id);
|
|
|
|
|
+ if (nodeMap[key]) {
|
|
|
|
|
+ var firstRecord = nodeMap[key];
|
|
|
|
|
+ duplicateMap[key] = true;
|
|
|
|
|
+ duplicates.push({
|
|
|
|
|
+ id: record.id,
|
|
|
|
|
+ first: {
|
|
|
|
|
+ dataIndex: firstRecord.index,
|
|
|
|
|
+ rowNumber: firstRecord.index + 1,
|
|
|
|
|
+ data: firstRecord.data
|
|
|
|
|
+ },
|
|
|
|
|
+ duplicate: {
|
|
|
|
|
+ dataIndex: record.index,
|
|
|
|
|
+ rowNumber: record.index + 1,
|
|
|
|
|
+ data: record.data
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ addProblem('duplicate-id', record, idField, record.id, undefined,
|
|
|
|
|
+ 'ID ' + record.id + ' 重复:datas[' + firstRecord.index + '](第 ' +
|
|
|
|
|
+ (firstRecord.index + 1) + ' 条)和 datas[' + record.index + '](第 ' +
|
|
|
|
|
+ (record.index + 1) + ' 条)是两个不同节点,请修改其中一个节点的 ID');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ nodeMap[key] = record;
|
|
|
|
|
+ records.push(record);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 先修正无效父节点,再检查父子关系是否成环。
|
|
|
|
|
+ records.forEach(function (record) {
|
|
|
|
|
+ var parent = nodeMap[valueKey(record.pid)];
|
|
|
|
|
+ record.fixedPid = record.pid;
|
|
|
|
|
+ if (sameValue(record.pid, rootId)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sameValue(record.pid, record.id) || !parent) {
|
|
|
|
|
+ record.fixedPid = rootId;
|
|
|
|
|
+ suggestUpdate(record, pidField, rootId);
|
|
|
|
|
+ addProblem(sameValue(record.pid, record.id) ? 'parent-self' : 'parent-missing',
|
|
|
|
|
+ record, pidField, record.pid, rootId,
|
|
|
|
|
+ sameValue(record.pid, record.id) ? '父节点指向自身,应改为根节点' : '父节点不存在,应改为根节点');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ records.forEach(function (record) {
|
|
|
|
|
+ var current = record;
|
|
|
|
|
+ var path = [];
|
|
|
|
|
+ var pathIndex = Object.create(null);
|
|
|
|
|
+ while (current && !sameValue(current.fixedPid, rootId)) {
|
|
|
|
|
+ var currentKey = valueKey(current.id);
|
|
|
|
|
+ if (pathIndex[currentKey] !== undefined) {
|
|
|
|
|
+ var breaker = path[path.length - 1];
|
|
|
|
|
+ var cycle = path.slice(pathIndex[currentKey]).map(function (item) {
|
|
|
|
|
+ return item.id;
|
|
|
|
|
+ });
|
|
|
|
|
+ breaker.fixedPid = rootId;
|
|
|
|
|
+ suggestUpdate(breaker, pidField, rootId);
|
|
|
|
|
+ addProblem('parent-cycle', breaker, pidField, breaker.pid, rootId,
|
|
|
|
|
+ '父节点形成环:' + cycle.join(' -> ') + ',建议断开此节点并改为根节点');
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ pathIndex[currentKey] = path.length;
|
|
|
|
|
+ path.push(current);
|
|
|
|
|
+ current = nodeMap[valueKey(current.fixedPid)];
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 按修正后的父节点分组,在每组内恢复为一条完整、无环、无分叉的兄弟链。
|
|
|
|
|
+ var groups = Object.create(null);
|
|
|
|
|
+ records.forEach(function (record) {
|
|
|
|
|
+ var groupKey = valueKey(record.fixedPid);
|
|
|
|
|
+ if (!groups[groupKey]) {
|
|
|
|
|
+ groups[groupKey] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ groups[groupKey].push(record);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(groups).forEach(function (groupKey) {
|
|
|
|
|
+ var siblings = groups[groupKey];
|
|
|
|
|
+ var siblingMap = Object.create(null);
|
|
|
|
|
+ var nextMap = Object.create(null);
|
|
|
|
|
+ var incoming = Object.create(null);
|
|
|
|
|
+ var visited = Object.create(null);
|
|
|
|
|
+ var ordered = [];
|
|
|
|
|
+ var stackRiskLimit = 1000;
|
|
|
|
|
+
|
|
|
|
|
+ siblings.forEach(function (record) {
|
|
|
|
|
+ siblingMap[valueKey(record.id)] = record;
|
|
|
|
|
+ incoming[valueKey(record.id)] = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+ siblings.forEach(function (record) {
|
|
|
|
|
+ var next = sameValue(record.nid, rootId) ? null : nodeMap[valueKey(record.nid)];
|
|
|
|
|
+ if (next && siblingMap[valueKey(next.id)] && !sameValue(next.id, record.id)) {
|
|
|
|
|
+ nextMap[valueKey(record.id)] = next;
|
|
|
|
|
+ incoming[valueKey(next.id)]++;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nextMap[valueKey(record.id)] = null;
|
|
|
|
|
+ if (!sameValue(record.nid, rootId)) {
|
|
|
|
|
+ addProblem(next && sameValue(next.id, record.id) ? 'next-self' : 'next-invalid',
|
|
|
|
|
+ record, nidField, record.nid, undefined,
|
|
|
|
|
+ next ? 'NextSiblingID 指向不同父节点下的节点或自身' : 'NextSiblingID 指向不存在的节点');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ Object.keys(incoming).forEach(function (key) {
|
|
|
|
|
+ if (incoming[key] > 1) {
|
|
|
|
|
+ var target = siblingMap[key];
|
|
|
|
|
+ addProblem('next-branch', target, nidField, target.id, undefined,
|
|
|
|
|
+ '有 ' + incoming[key] + ' 个节点同时指向该节点,兄弟链产生分叉');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var appendChain = function (start) {
|
|
|
|
|
+ var current = start;
|
|
|
|
|
+ var chain = [];
|
|
|
|
|
+ var chainIndex = Object.create(null);
|
|
|
|
|
+ while (current && !visited[valueKey(current.id)]) {
|
|
|
|
|
+ chainIndex[valueKey(current.id)] = chain.length;
|
|
|
|
|
+ chain.push(current);
|
|
|
|
|
+ visited[valueKey(current.id)] = true;
|
|
|
|
|
+ ordered.push(current);
|
|
|
|
|
+ current = nextMap[valueKey(current.id)];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (current && chainIndex[valueKey(current.id)] !== undefined) {
|
|
|
|
|
+ var cycleStart = chainIndex[valueKey(current.id)];
|
|
|
|
|
+ var cycleRecords = chain.slice(cycleStart);
|
|
|
|
|
+ var cycleEnd = cycleRecords[cycleRecords.length - 1];
|
|
|
|
|
+ addProblem('next-cycle', cycleEnd, nidField, cycleEnd.nid, undefined,
|
|
|
|
|
+ '兄弟节点的 NextSiblingID 形成环:' + cycleRecords.map(function (item) {
|
|
|
|
|
+ return item.id;
|
|
|
|
|
+ }).concat([current.id]).join(' -> ') + ',请按 updateDatas 修复');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (chain.length > stackRiskLimit) {
|
|
|
|
|
+ stackRisks.push({
|
|
|
|
|
+ parentId: start.fixedPid,
|
|
|
|
|
+ chainLength: chain.length,
|
|
|
|
|
+ firstNodeId: chain[0].id,
|
|
|
|
|
+ lastNodeId: chain[chain.length - 1].id,
|
|
|
|
|
+ message: '同一父节点下连续兄弟链有 ' + chain.length +
|
|
|
|
|
+ ' 个节点,旧版 moveNode 逐节点递归会导致 Maximum call stack size exceeded;请使用迭代版 moveNode'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ siblings.forEach(function (record) {
|
|
|
|
|
+ if (incoming[valueKey(record.id)] === 0) {
|
|
|
|
|
+ appendChain(record);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ // 剩余节点属于环,或处于已合并链的未访问部分;按原数据顺序接到末尾。
|
|
|
|
|
+ siblings.forEach(function (record) {
|
|
|
|
|
+ appendChain(record);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ ordered.forEach(function (record, index) {
|
|
|
|
|
+ var expectedNid = index === ordered.length - 1 ? rootId : ordered[index + 1].id;
|
|
|
|
|
+ if (!sameValue(record.nid, expectedNid)) {
|
|
|
|
|
+ suggestUpdate(record, nidField, expectedNid);
|
|
|
|
|
+ addProblem('next-order', record, nidField, record.nid, expectedNid,
|
|
|
|
|
+ '兄弟链不连续,按可恢复顺序修改 NextSiblingID');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ var updateDatas = Object.keys(updateMap).map(function (key) {
|
|
|
|
|
+ return { type: 'update', data: updateMap[key] };
|
|
|
|
|
+ });
|
|
|
|
|
+ var result = {
|
|
|
|
|
+ valid: problems.length === 0,
|
|
|
|
|
+ canAutoFix: Object.keys(duplicateMap).length === 0,
|
|
|
|
|
+ problems: problems,
|
|
|
|
|
+ duplicates: duplicates,
|
|
|
|
|
+ stackRisks: stackRisks,
|
|
|
|
|
+ updateDatas: updateDatas
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof console !== 'undefined') {
|
|
|
|
|
+ if (result.valid) {
|
|
|
|
|
+ console.log('树结构检查通过,共 ' + records.length + ' 个节点');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.group('树结构检查:发现 ' + problems.length + ' 个问题');
|
|
|
|
|
+ console.table(problems);
|
|
|
|
|
+ duplicates.forEach(function (item, index) {
|
|
|
|
|
+ console.group('重复 ID 问题 ' + (index + 1) + ':ID = ' + item.id);
|
|
|
|
|
+ console.error('以下两个节点的 ID 相同,必须修改其中一个:');
|
|
|
|
|
+ console.log('节点 A:datas[' + item.first.dataIndex + '],第 ' +
|
|
|
|
|
+ item.first.rowNumber + ' 条原始数据', item.first.data);
|
|
|
|
|
+ console.log('节点 B:datas[' + item.duplicate.dataIndex + '],第 ' +
|
|
|
|
|
+ item.duplicate.rowNumber + ' 条原始数据', item.duplicate.data);
|
|
|
|
|
+ console.groupEnd();
|
|
|
|
|
+ });
|
|
|
|
|
+ console.log('建议修改数据(newCheck 只生成建议,不会自动修改):');
|
|
|
|
|
+ console.table(updateDatas.map(function (item) { return item.data; }));
|
|
|
|
|
+ if (!result.canAutoFix) {
|
|
|
|
|
+ console.warn('存在重复 ID,必须先人工分配新 ID,其他修改建议才可安全应用');
|
|
|
|
|
+ }
|
|
|
|
|
+ console.groupEnd();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (stackRisks.length) {
|
|
|
|
|
+ console.group('树排序递归栈风险:发现 ' + stackRisks.length + ' 条过长兄弟链');
|
|
|
|
|
+ console.table(stackRisks);
|
|
|
|
|
+ console.warn('这不一定是数据错误;旧版递归 moveNode 会因此堆栈溢出,改为迭代实现即可正常显示');
|
|
|
|
|
+ console.groupEnd();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ //检查树结构数据有没问题
|
|
|
|
|
+ Tree.prototype.check = function (roots) {
|
|
|
|
|
+ return isValid(roots);
|
|
|
|
|
+ function isValid(nodes) {
|
|
|
|
|
+ for (let node of nodes) {
|
|
|
|
|
+ if (node.data.ParentID != -1 &&
|
|
|
|
|
+ (!node.parent || node.parent.data.ID !== node.data.ParentID)) {
|
|
|
|
|
+ console.log(`${node.serialNo() + 1}:${node.data.name} parent对应错误`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (node.data.ParentID == -1 && node.parent) {
|
|
|
|
|
+ console.log(`${node.serialNo() + 1}:${node.data.name} 不应有parent`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (node.data.NextSiblingID != -1 &&
|
|
|
|
|
+ (!node.nextSibling || node.nextSibling.data.ID !== node.data.NextSiblingID)) {
|
|
|
|
|
+ console.log(`${node.serialNo() + 1}:${node.data.name} next对应错误`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (node.data.NextSiblingID == -1 && node.nextSibling) {
|
|
|
|
|
+ console.log(`${node.serialNo() + 1}:${node.data.name} 不应有next`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ let sameDepthNodes = node.parent ? node.parent.children : roots,
|
|
|
|
|
+ nodeIdx = sameDepthNodes.indexOf(node),
|
|
|
|
|
+ nextIdx = sameDepthNodes.indexOf(node.nextSibling);
|
|
|
|
|
+ if (nodeIdx != -1 && nextIdx != -1 && nodeIdx > nextIdx) {
|
|
|
|
|
+ console.log(`${node.serialNo() + 1}:${node.data.name} node索引大于next索引`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ // nextSibling跟parent children的下一节点对应不上
|
|
|
|
|
+ if (nodeIdx != -1 &&
|
|
|
|
|
+ (nodeIdx === sameDepthNodes.length - 1 && nextIdx != -1) ||
|
|
|
|
|
+ (nodeIdx !== sameDepthNodes.length - 1 && nodeIdx + 1 !== nextIdx)) {
|
|
|
|
|
+ console.log(`${node.serialNo() + 1}:${node.data.name} nextSibling与树显示的下一节点对应不上`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (node.children.length) {
|
|
|
|
|
+ let v = isValid(node.children);
|
|
|
|
|
+ if (!v) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return new Tree(setting);
|
|
return new Tree(setting);
|
|
|
},
|
|
},
|
|
|
updateType: { update: 'update', new: 'new', delete: 'delete' }
|
|
updateType: { update: 'update', new: 'new', delete: 'delete' }
|