import { isEqual, uniqueId, cloneDeep } from 'lodash'; import * as React from 'react'; import loadjs from 'loadjs'; import classnames from 'classnames'; import { RightMenu, RightMenuStore } from './rightMenu'; import { WeaSlideModal, WeaDialog } from 'ecCom'; import './index.less'; const getLength = (str) => { let realLength = 0, len = str.length, charCode = -1; for (let i = 0; i < len; i++) { charCode = str.charCodeAt(i); if (charCode >= 0 && charCode <= 128) realLength += 1; else realLength += 2; } return realLength; }; const getSubStr = (str, start, length) => { let rt = [], currentLength = 0, len = str.length, charCode = -1; for (let i = 0; i < len; i++) { charCode = str.charCodeAt(i); if (charCode >= 0 && charCode <= 128) { currentLength++; } else { currentLength += 2; } if (currentLength > start && currentLength <= length) { rt.push(str.charAt(i)); } } return rt.join(''); } const getShortStr = (ele, data, w = 100) => { const length = parseInt(w / 6); const datalength = data ? getLength(data) : 0; if (length >= datalength) { return data; } return getSubStr(data, 0, length) + '...'; } export default class D3Tree extends React.Component { totalNodes = 0; maxLabelLength = 0; i = 0; selectedPath = []; currentPath = []; overedPath = []; static defaultProps = { widthToggle: true, showRight: true, layout: '0', showMenu: false } constructor(props) { super(props); this.state = { duration: 750, hoverNode: undefined, id: `d3TreeContainer_${new Date().getTime()}_${uniqueId()}`, width: 220, height: 100 } this.didMountTree = false; this.rightMenuStore = new RightMenuStore(); } render() { const { showRight, style, showMenu } = this.props; const menu = [ { key: '1', icon: , content: '按钮 1', }, { key: '2', icon: , content: '按钮 2', } ]; return (
{ this.props.closeRight(false) }}> {/* */}
this.container = ref} id={this.state.id} className="d3-tree" style={style} onContextMenu={showMenu ? this.onContextMenu : undefined} /> {/* */} {/*
{this.props.showRight && this.renderRight()}
*/}
e.stopPropagation()} className={"Prj-WeaSlideModal"}> )} onClose={() => { this.props.closeRight(false) }} onAnimationEnd={() => console.log('onAnimationEnd')} />
{/* this.props.closeDialog(false) } visible={this.props.showDialog} style={{ width: 1000, height: 600 }} title={'活动计划'} hasScroll url={this.renderRight() !== 'function'&& this.renderRight()} > */}
this.selection = ref}>
) } onContextMenu = (e) => { if (this.isClickNode && this.opreateNode) { const left = e.clientX; const top = e.clientY; this.showRightMenu(); this.rightMenuStore.show(left, top, this.opreateNode.id === this.root.id, this.getHasChild(this.opreateNode), this.getChildVisible(this.opreateNode)); } this.isClickNode = false; e.stopPropagation && e.stopPropagation(); e.preventDefault && e.preventDefault(); e.nativeEvent && e.nativeEvent.preventDefault(); } showRightMenu = () => { if (!this.menuWrapper) { this.menuWrapper = document.createElement('div'); document.body.appendChild(this.menuWrapper); } ReactDOM.render(, this.menuWrapper); } clickAnyWhere = (e) => { this.rightMenuStore.hide(); if (this.startMove && this.selection) { this.startMove = false; this.selection.style.display = 'none'; const top = parseInt(this.selection.style.top); const left = parseInt(this.selection.style.left); const width = parseInt(this.selection.style.width); const height = parseInt(this.selection.style.height); this.selection.style.height = '0px'; this.selection.style.width = '0px'; } } mouseup = (d) => { const e = window.event; if (e.button === 2) { this.opreateNode = d; this.isClickNode = true; this.click(d); } } renderRight = () => { if (typeof this.props.renderRight === 'function') { if (this.selectedNode && this.props.renderRight) { return this.props.renderRight(this.selectedNode); } else { return null; } } else { return this.props.renderRight; } } diableMouseOut = () => { this.canTriggerMouseOut = false; } enableMouseOut = () => { this.canTriggerMouseOut = true; this.outNode(); } componentWillReceiveProps(nextProps) { if (!isEqual(this.props.data, nextProps.data) || this.props.layout !== nextProps.layout && this.container && (this.didMountTree = false, this.container.innerHTML = "", true)) { if (nextProps.data) { clearTimeout(this.initTimer); this.initTimer = setTimeout(() => { this.initTree(nextProps.data); }, 100); } } if (this.props.scale !== nextProps.scale) { if (this.svgGroup) { const transform = this.svgGroup.attr("transform"); if (transform) { const scale = nextProps.scale / 10; this.svgGroup.attr("transform", `${transform.split('scale')[0]}scale(${scale})`); } } } } componentWillUnmount() { window.removeEventListener('resize', this.resize); document.removeEventListener('mouseup', this.clickAnyWhere); if (this.container) { /* this.container.removeEventListener('mousedown', this.onMouseDown); this.container.removeEventListener('mousemove', this.onMouseMove); */ } if (this.menuWrapper) { ReactDOM.unmountComponentAtNode(this.menuWrapper); document.body.removeChild(this.menuWrapper); delete this.menuWrapper; } } componentDidMount() { window.addEventListener('resize', this.resize); document.addEventListener('mouseup', this.clickAnyWhere); if (this.container) { /* this.container.addEventListener('mousedown', this.onMouseDown); this.container.addEventListener('mousemove', this.onMouseMove); */ } if (!loadjs.isDefined('weaEdcD3js')) { loadjs(['/edc/d3/d3.v3.js'], 'weaEdcD3js', { success: () => { if (this.container && this.props.data) { const _this = this; setTimeout(function(){ _this.initTree(_this.props.data); _this.resize(); _this.click(_this.root); },1000); } } }); } else { if (this.container && this.props.data) { const _this = this; setTimeout(function(){ _this.initTree(_this.props.data); _this.resize(); _this.click(_this.root); },1000); } } } onMouseDown = (e) => { if (e.button === 0 && this.selection && e.target.tagName === 'svg') { this.selectionLeft = e.clientX; this.selectionTop = e.clientY; this.selection.style.display = 'block'; this.selection.style.top = this.selectionTop + 'px'; this.selection.style.left = this.selectionLeft + 'px'; this.startMove = true; } } onMouseMove = (e) => { if (this.startMove) { this.selection.style.height = (e.y - this.selectionTop) + 'px'; this.selection.style.width = (e.x - this.selectionLeft) + 'px'; e.stopPropagation && e.stopPropagation(); e.preventDefault && e.preventDefault(); e.nativeEvent && e.nativeEvent.preventDefault(); } } resize = () => { clearTimeout(this.resizeTimer); this.resizeTimer = setTimeout(() => { const viewerHeight = this.container.clientHeight; const viewerWidth = this.container.clientWidth; d3.select(`#${this.state.id}>svg`) .attr("width", viewerWidth) .attr("height", viewerHeight); }, 100); } getSize = () => { const { layout } = this.props; const { width, height } = this.state; if (layout === '1') { // 上 return { nodeSize: [width + 10, height], width, height, deep: width } } else if (layout === '3') { // 下 return { nodeSize: [width + 10, height], width, height, deep: width * -1 } } else if (layout === '0') { // 左 return { nodeSize: [height + 10, width], width, height, deep: width * 2, } } else if (layout === '2') { // 右 return { nodeSize: [height + 10, width], width, height, deep: width * -2, } } } searchNode = (name, nodes = [this.root]) => { const searchedNode = []; nodes.forEach(node => { if (node.name.toLowerCase().indexOf(name) > -1) { searchedNode.push(node); } if (node.children) { searchedNode.push(...this.searchNode(name, node.children)); } if (node._children) { searchedNode.push(...this.searchNode(name, node._children)); } }); return searchedNode; } initTree(treeData) { const { layout } = this.props; const { width, height } = this.state; const { nodeSize } = this.getSize(); if (this.container && window.d3) { const viewerHeight = this.container.clientHeight; const viewerWidth = this.container.clientWidth; this.tree = d3.layout.tree().nodeSize(nodeSize); // define a d3 diagonal projection for use by the node paths later on. this.diagonal = d3.svg.diagonal() .projection((d) => { return (layout === '1' || layout === '3') ? [d.x + width / 2, d.y + height / 2] : [d.y + width / 2, d.x + height / 2]; }); this.sortTree(); if (!this.didMountTree) { const zoom = (e) => { this.svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); if (this.props.zoom) { this.props.zoom(d3.event.scale) } } if (this.props.zoom) { this.props.zoom(1) } this.zoomListener = d3.behavior.zoom().scaleExtent([0.1, 2]).on("zoom", zoom); // define the baseSvg, attaching a class for styling and the zoomListener this.baseSvg = d3.select("#" + this.state.id).append("svg") .attr("width", viewerWidth) .attr("height", viewerHeight) .attr("class", "overlay") .call(this.zoomListener); this.svgGroup = this.baseSvg.append("g"); } // Define the drag listeners for drag/drop behaviour of nodes. // Define the root const { x0 = viewerHeight / 2, y0 = 0 } = this.root || {}; this.root = cloneDeep(treeData) this.root.x0 = x0 this.root.y0 = y0; // Layout the tree initially and center on the root node. this.update(this.root); if (!this.didMountTree) { this.centerNode(this.root); } //选中的节点在不在树中时 选中根节点 const curNodeId = this.selectedNode.id; const treeNodeIds = [treeData.id]; treeData.children.map(c => { treeNodeIds.push(c.id); }) if (treeNodeIds.indexOf(curNodeId) < 0) { this.click(this.root); } this.didMountTree = true; } else { setTimeout(() => { this.initTree(treeData); }, 50); } } centerNode = (source) => { const { layout } = this.props; const { width, height } = this.state; const viewerHeight = this.container.clientHeight; const viewerWidth = this.container.clientWidth; const scale = this.zoomListener.scale(); const isRoot = source === this.root; this.selectedNode = source; let x = 0; let y = 0; if (layout === '1') { y = -source.y0; x = -source.x0; x = x * scale + viewerWidth / 2; if (isRoot) { y = y * scale + height; } else { y = y * scale + viewerHeight / 2; } } else if (layout === '3') { y = -source.y0; x = -source.x0; x = x * scale + viewerWidth / 2; if (isRoot) { y = y * scale + viewerHeight - height * 2; } else { y = y * scale + viewerHeight / 2; } } else if (layout === '2') { x = -source.y0; y = -source.x0; if (isRoot) { x = x * scale + viewerWidth - width * 2; } else { x = x * scale + viewerWidth / 2; } y = y * scale + viewerHeight / 2; } else { x = -source.y0; y = -source.x0; if (isRoot) { x = x * scale + width; } else { x = x * scale + viewerWidth / 2; } y = y * scale + viewerHeight / 2; } this.svgGroup.transition() .duration(this.state.duration) .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")"); this.zoomListener.scale(scale); this.zoomListener.translate([x, y]); this.click(source); } collapse = (d) => { if (d.children) { d._children = d.children; d._children.forEach(collapse); d.children = null; } } expand = (d) => { if (d._children) { d.children = d._children; d.children.forEach(expand); d._children = null; } } sortTree = () => { this.tree.sort((a, b) => { return parseInt(a.id) - parseInt(b.id); }); } getXY = (x, y) => { const { layout } = this.props; if (layout === '1' || layout === '3') { // 上下 return `${x},${y}`; } else { return `${y},${x}`; } } update = (source) => { if (!this.tree) { setTimeout(() => { this.update(source); }, 50); return; } const { desc, desc2, desc3, showname, showSubmitCount = true, nameTitle } = this.props; const { duration } = this.state; const { deep, width, height, textAnchor } = this.getSize(); // Compute the new tree layout. window.tree = this.tree; const nodes = this.tree.nodes(this.root).reverse(); const links = this.tree.links(nodes); const isShownameFunc = typeof showname === 'function'; const isDescFunc = typeof desc === 'function'; const isDesc2Func = typeof desc2 === 'function'; const isDesc3Func = typeof desc3 === 'function'; // Normalize for fixed-depth. nodes.forEach((d) => { d.y = d.depth * deep; if (d.current) { this.currentPath = this.getLinksToParents(d); } }); // Update the nodes… const node = this.svgGroup.selectAll("g.node") .data(nodes, (d) => d.id); // Enter any new nodes at the parent's previous position. const nodeEnter = node.enter().append("g") .attr("class", 'node') .attr("transform", d => `translate(${this.getXY(source.x0, source.y0)})`) .on("click", this.click) .on('mouseup', this.mouseup); nodeEnter.append("rect") .attr('class', 'node-rect') .attr("width", width) .attr("height", height) .attr("stroke", "#999") .attr("stroke-width", 2) .attr("fill", "#fff") .attr("rx", 2) .attr("ry", 2); nodeEnter.append("rect") .attr("class", 'flag') .attr("x", 1) .attr("y", 1) .attr("width", 6) .attr("height", height - 2) .attr("fill", "rgb(230, 228, 228)") nodeEnter.append("rect") .attr("class", 'flag2') .attr("x", 1) .attr("y", (d)=>{ if(d.type=="task"){ // console.log((height - 2)*(d.finish/100),222,height,d.finish,d.id); let finish = d.finish == -1 ? 0 : d.finish; return height - height*(finish/100) + 1*(finish/100) }else{ return 1 } }) .attr("width", 6) .attr("height", (d)=>{ if(d.type=="task"){ // console.log((height - 2)*(d.finish/100),222,height,d.finish,d.id); let finish = d.finish == -1 ? 0 : d.finish; return (height - 2)*(finish/100) }else{ // console.log((height - 2)); return height - 2 } }) .attr("fill", "#fff") .style("fill", (d) => { if(d.type=="task"){ if(d.overstr!==""){ return "red"; }else{ return "#52c41a"; } }else{ return d.type=="prj" ? "#f7f7f7" : (d.type=="task" && d.isfinish == 1 ? "#52c41a" : '#f7f7f7'); } }); const textg = nodeEnter.append('g').attr('class', 'text-g'); textg.append("text") .attr("x", 20) .attr("y", 20) .attr("dy", ".5em") .attr("text-anchor", textAnchor) .text(function (d) { return getShortStr(this, isShownameFunc ? showname(d) : d[showname],120); }) .on('click', this.clickTitle); textg.append('title').text(d => nameTitle ? nameTitle : isShownameFunc ? showname(d) : d[showname]); nodeEnter.append("text") .attr("display", d => { let all = 0; if (d.children) { all = d.children.length; } else if (d._children) { all = d._children.length; } return all ? 'block' : 'none'; }) .attr("class", 'count') .attr("x", width - 30) .attr("y", 20) .attr("dy", ".35em") .attr("text-anchor", textAnchor) .text(d => { let submitCount = 0; let all = 0; if (d.children) { submitCount = d.children.filter(d => d.isfinish).length; all = d.children.length; } else if (d._children) { submitCount = d._children.filter(d => d.isfinish).length; all = d._children.length; } if (showSubmitCount) { return `${submitCount}/${all}` } else { return `${all}` } }); const descg = nodeEnter.append('g').attr('class', 'desc-g'); descg.append("text") .attr("class", 'desc') .attr("x", 20) .attr("y", 45) .attr("dy", ".35em") .attr("text-anchor", textAnchor).text(function (d) { return getShortStr(this, isDescFunc ? desc(d) : d[desc], 120); }); descg.append('title').text(d => isDescFunc ? desc(d) : d[desc]); const descg2 = nodeEnter.append('g').attr('class', 'desc-g2'); descg2.append("text") .attr("class", 'desc') .attr("x", 20) .attr("y", 65) .attr("dy", ".35em") .attr("text-anchor", textAnchor).text(function (d) { return d[desc2]; }); descg2.append('title').text(d => isDesc2Func ? desc(d) : d[desc2]); const descg3 = nodeEnter.append('g').attr('class', 'desc-g3'); descg3.append("text") .attr("class", 'desc') .attr("x", 20) .attr("y", 85) .attr("dy", ".35em") .attr("fill", "rgb(109, 106, 236") .attr("text-anchor", textAnchor).text(function (d) { return getShortStr(this, isDesc3Func ? desc(d) : d[desc3], 120); }); descg3.append('title').text(d => isDesc3Func ? desc(d) : d[desc3]); // descText.append('svg:title').text(d=>d[desc]); // Transition nodes to their new position. const nodeUpdate = node.transition() .duration(duration) .attr("transform", d => `translate(${this.getXY(d.x, d.y)})`) .attr("class", (d) => { if (this.selectedNode && d.id === this.selectedNode.id) { return 'node node-selected' } else { return 'node'; } }); nodeEnter.append("rect") .attr("class", 'flag2') .attr("x", 1) .attr("y", (d)=>{ if(d.type=="task"){ // console.log((height - 2)*(d.finish/100),222,height,d.finish,d.id); let finish = d.finish == -1 ? 0 : d.finish; return height - height*(finish/100) + 1*(finish/100) }else{ return 1 } }) .attr("width", 6) .attr("height", (d)=>{ if(d.type=="task"){ // console.log((height - 2)*(d.finish/100),222,height,d.finish,d.id); let finish = d.finish == -1 ? 0 : d.finish; return (height - 2)*(finish/100) }else{ // console.log((height - 2)); return height - 2 } }) .attr("fill", "#fff") .style("fill", (d) => { if(d.type=="task"){ if(d.overstr!==""){ return "red"; }else{ return "#52c41a"; } }else{ return d.type=="prj" ? "#f7f7f7" : (d.type=="task" && d.isfinish == 1 ? "#52c41a" : '#f7f7f7'); } }); nodeUpdate.select("rect.flag2") .attr("y", (d)=>{ if(d.type=="task"){ // console.log((height - 2)*(d.finish/100),222,height,d.finish,d.id); let finish = d.finish == -1 ? 0 : d.finish; return height - height*(finish/100) + 1*(finish/100) }else{ return 1 } }) .attr("height", (d)=>{ if(d.type=="task"){ // console.log((height - 2)*(d.finish/100),222,height,d.finish,d.id); let finish = d.finish == -1 ? 0 : d.finish; return (height - 2)*(finish/100) }else{ // console.log((height - 2)); return height - 2 } }) .style("fill", (d) => { if(d.type=="task"){ if(d.overstr!==""){ return "red"; }else{ return "#52c41a"; } }else{ return d.type=="prj" ? "#f7f7f7" : (d.type=="task" && d.isfinish == 1 ? "#52c41a" : '#f7f7f7'); } }); nodeUpdate.select("rect.node-rect") .attr("width", width) .attr("height", height) .attr("stroke", "#999") .attr("stroke-width", 2) .attr("fill", "#fff"); nodeUpdate.select("text").style("fill-opacity", 1); nodeUpdate.select("g.text-g>text").text(function (d) { return getShortStr(this, isShownameFunc ? showname(d) : d[showname]); }); nodeUpdate.select("g.text-g>title").text(function (d) { return nameTitle ? nameTitle : isShownameFunc ? showname(d) : d[showname]; }) nodeUpdate.select("g.desc-g>text.desc").text(function (d) { return getShortStr(this, isDescFunc ? desc(d) : d[desc], 120); }); nodeUpdate.select("g.desc-g2>text.desc").text(function (d) { return d[desc2]; }); nodeUpdate.select("g.desc-g3>text.desc").text(function (d) { return getShortStr(this, isDesc3Func ? desc(d) : d[desc3], 120); }); nodeUpdate.select("g.desc-g>title").text(function (d) { return isDescFunc ? desc(d) : d[desc]; }); nodeUpdate.select("g.desc-g>title").text(d => isDescFunc ? desc(d) : d[desc]); nodeUpdate.select('text.count').text(d => { let submitCount = 0; let all = 0; if (d.children) { submitCount = d.children.filter(d => d.isfinish).length; all = d.children.length; } else if (d._children) { submitCount = d._children.filter(d => d.isfinish).length; all = d._children.length; } if (showSubmitCount) { return `${submitCount}/${all}` } else { return `${all}` } }).attr("display", d => { let all = 0; if (d.children) { all = d.children.length; } else if (d._children) { all = d._children.length; } return all ? 'block' : 'none'; }) nodeUpdate.select("g.count>text").text(d => { let submitCount = 0; let all = 0; if (d.children) { submitCount = d.children.filter(d => d.isfinish).length; all = d.children.length; } else if (d._children) { submitCount = d._children.filter(d => d.isfinish).length; all = d._children.length; } if (showSubmitCount) { return `${submitCount}/${all}` } else { return `${all}` } }).attr("display", d => { let all = 0; if (d.children) { all = d.children.length; } else if (d._children) { all = d._children.length; } return all ? 'block' : 'none'; }) // Transition exiting nodes to the parent's new position. const nodeExit = node.exit().transition() .duration(duration) .attr("transform", d => `translate(${this.getXY(source.x, source.y)})`) .remove(); nodeExit.select("rect") .attr("width", width) .attr("height", height) .attr("stroke", "#ccc") .attr("stroke-width", 2); nodeExit.select("text"); // Update the links… const link = this.svgGroup.selectAll("path.link") .data(links, d => d.target.id) .attr('class', "link"); // Enter any new links at the parent's previous position. link.enter().insert("path", "g") .attr('class', "link") .attr("stroke", d => { const { source: { id: from }, target: { id: to } } = d; if (this.selectedPath.indexOf(`${from},${to}`) > -1) { return "#2db7f5" } else if (this.currentPath.indexOf(`${from},${to}`) > -1) { return "#2db7f5"; } return "#ccc" }) .attr('fill', "none") .attr("stroke-width", 1.5) .attr("x", width / 2) .attr("y", height / 2) .attr("d", (d) => { const o = { x: source.x0, y: source.y0 }; return this.diagonal({ source: o, target: o }); }); // Transition links to their new position. link.transition() .duration(duration) .attr("d", this.diagonal) .attr("stroke", d => { const { source: { id: from }, target: { id: to } } = d; if (this.selectedPath.indexOf(`${from},${to}`) > -1) { return "#2db7f5" } else if (this.currentPath.indexOf(`${from},${to}`) > -1) { return "#2db7f5"; } return "#ccc" }); // Transition exiting nodes to the parent's new position. link.exit().transition() .duration(duration) .attr("d", (d) => { var o = { x: source.x, y: source.y }; return this.diagonal({ source: o, target: o }); }) .remove(); // Stash the old positions for transition. nodes.forEach((d) => { d.x0 = d.x; d.y0 = d.y; }); } clickTitle = (d) => { if (this.props.onClickTitle) { this.props.onClickTitle(d); window.event.stopPropagation(); } } click = (d) => { if (d) { this.selectedPath = this.getLinksToParents(d); this.selectedNode = d; // d = this.toggleChildren(d); this.update(d); if (this.props.onClick) { this.props.onClick(d, this.getHasChild(d)); } } } getLinksToParents = (d) => { const selectedPath = []; let current = d; while (current) { selectedPath.push(`${current.parentid},${current.id}`); current = current.parent; } return selectedPath; } toggleChildren = () => { const d = this.opreateNode; if (d) { if (d.children) { d._children = d.children; d.children = null; } else if (d._children) { d.children = d._children; d._children = null; } this.update(d); } } getChildVisible = (d) => { return !!d.children; } getHasChild = (d) => { return !!d.children || !!d._children; } }