diff --git a/package.json b/package.json index e219c99..9926ca4 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "_test": "tape 'test/**/*-test.js'", "_prepublish": "npm run test && uglifyjs build/d3-org-chart.js -c -m -o build/d3-org-chart.min.js", "_postpublish": "zip -j build/d3-org-chart.zip -- LICENSE README.md build/d3-org-chart.js build/d3-org-chart.min.js", - "pretest": "rm -rf build && mkdir build && rollup -g d3-selection:d3,d3-array:d3,d3-hierarchy:d3,d3-zoom:d3,d3-flextree:d3,d3-shape:d3,d3-group:d3 -f umd -n d3 -o build/d3-org-chart.js -- index.js", + "pretest": "rm -rf build && mkdir build && rollup -g d3-drag:d3,d3-selection:d3,d3-array:d3,d3-hierarchy:d3,d3-zoom:d3,d3-flextree:d3,d3-shape:d3,d3-group:d3 -f umd -n d3 -o build/d3-org-chart.js -- index.js", "test": "tape 'test/**/*-test.js'", "prepublish": "npm run test && uglifyjs build/d3-org-chart.js -c -m -o build/d3-org-chart.min.js", "postpublish": "zip -j build/d3-org-chart.zip -- LICENSE README.md build/d3-org-chart.js build/d3-org-chart.min.js" @@ -51,6 +51,7 @@ "dependencies": { "d3-selection": "3", "d3-array": "3", + "d3-drag": "3", "d3-hierarchy": "3", "d3-zoom": "3", "d3-shape": "3", diff --git a/src/d3-org-chart.js b/src/d3-org-chart.js index 0a06871..f0ff7f4 100644 --- a/src/d3-org-chart.js +++ b/src/d3-org-chart.js @@ -1,13 +1,15 @@ -import { selection, select } from "d3-selection"; +import { selection, select, selectAll } from "d3-selection"; import { max, min, sum, cumsum } from "d3-array"; import { tree, stratify } from "d3-hierarchy"; import { zoom, zoomIdentity } from "d3-zoom"; import { flextree } from 'd3-flextree'; import { linkHorizontal } from 'd3-shape'; +import { drag } from "d3-drag"; const d3 = { selection, select, + selectAll, max, min, sum, @@ -17,7 +19,8 @@ const d3 = { zoom, zoomIdentity, linkHorizontal, - flextree + flextree, + drag } export class OrgChart { @@ -37,6 +40,12 @@ export class OrgChart { allowedNodesCount: {}, zoomBehavior: null, generateRoot: null, + dragNode: null, + dragTargetNode: null, + dragStartX: null, + dragStartY: null, + isDragStart: false, + /* INTENDED FOR PUBLIC OVERRIDE */ @@ -75,6 +84,16 @@ export class OrgChart { onZoomEnd: e => { }, // Callback for zoom & panning end onNodeClick: (d) => d, // Callback for node click onExpandOrCollapse: (d) => d, // Callback for node expand or collapse + enableDragDrop: (d) => {}, + onDragStart: (node, dragEvent) => {}, + onDrag: (node, dragEvent) => {}, + onDrop: (dragNode, targetNode, dragEvent) => {}, + onDragTarget: (dragNode, targetNode, dragEvent) => {}, + outDragTarget: (dragNode, targetNode, dragEvent) => {}, + onDragFilter: (node, dragEvent) => {}, + draggingClass: () => 'dragging', + draggableClass: () => 'draggable', + droppableClass: () => 'droppable', /* * Node HTML content generation , remember that you can access some helper methods: @@ -1057,6 +1076,26 @@ export class OrgChart { } } }); + + if (attrs.enableDragDrop()) { + const self = this; + nodeEnter.call( + d3.drag() + .filter(function (dragEvent, node) { + return this.classList.contains(attrs.draggableClass()) && attrs.onDragFilter.apply(this, [node, dragEvent]); + }) + .on('start', function (dragEvent, node) { + self.dragStart(this, dragEvent, node); + }) + .on('drag', function (dragEvent) { + self.drag(this, dragEvent); + }) + .on('end', function (dragEvent) { + self.dragEnd(this, dragEvent); + }) + ); + } + nodeEnter.each(attrs.nodeEnter) // Add background rectangle for the nodes @@ -1906,4 +1945,134 @@ export class OrgChart { d3.select(window).on(`resize.${attrs.id}`, null); attrs.svg && attrs.svg.selectAll("*").remove(); } + + + dragStart(element, dragEvent, node) { + const attrs = this.getChartState(); + attrs.dragNode = node; + + const width = dragEvent.subject.width; + const half = width / 2; + const x = dragEvent.x - half; + attrs.dragStartX = x; + attrs.dragStartY = parseFloat(dragEvent.y); + attrs.isDragStart = true; + + d3.select(element).classed(attrs.draggingClass(), true); + + attrs.onDragStart.apply(element, [node, dragEvent]); + } + + drag(element, dragEvent) { + const attrs = this.getChartState(); + if (!attrs.dragNode) return; + + const g = d3.select(element); + + // This condition is designed to run at the start of a drag only + if (attrs.isDragStart) { + attrs.isDragStart = false; + + // This sets the Z-Index above all other nodes, by moving the dragged node to be the last-child. + g.raise(); + + const descendants = dragEvent.subject.descendants(); + + // Remove links associated with the dragNode + const linksToRemove = [...(descendants || []), dragEvent.subject]; + attrs['linksWrapper'] + .selectAll('path.link') + .data(linksToRemove, (d) => attrs.nodeId(d)) + .remove(); + + // Remove all descendant nodes associated with the dragging node + const nodesToRemove = descendants.filter( + (x) => x.data.id !== dragEvent.subject.id + ); + if (nodesToRemove) { + attrs['nodesWrapper'] + .selectAll('g.node') + .data(nodesToRemove, (d) => attrs.nodeId(d)) + .remove(); + } + } + + attrs.dragTargetNode = null; + const cP = { + width: dragEvent.subject.width, + height: dragEvent.subject.height, + left: dragEvent.x, + right: dragEvent.x + dragEvent.subject.width, + top: dragEvent.y, + bottom: dragEvent.y + dragEvent.subject.height, + midX: dragEvent.x + dragEvent.subject.width / 2, + midY: dragEvent.y + dragEvent.subject.height / 2, + }; + + const allNodes = d3.selectAll(`g.node:not(.${attrs.draggingClass()})`); + + const lastTarget = attrs.dragTargetNode; + + allNodes + .filter(function (d2, i) { + const cPInner = { + left: d2.x, + right: d2.x + d2.width, + top: d2.y, + bottom: d2.y + d2.height, + }; + + if ( + cP.midX > cPInner.left && + cP.midX < cPInner.right && + cP.midY > cPInner.top && + cP.midY < cPInner.bottom && + this.classList.contains(attrs.droppableClass()) + ) { + attrs.dragTargetNode = d2; + return d2; + } + }) + + attrs.dragStartX += parseFloat(dragEvent.dx); + attrs.dragStartY += parseFloat(dragEvent.dy); + g.attr('transform', 'translate(' + attrs.dragStartX + ',' + attrs.dragStartY + ')'); + + if (lastTarget) { + attrs.outDragTarget.apply(element, [attrs.dragNode, lastTarget, dragEvent]); + } + + attrs.onDragTarget.apply(element, [attrs.dragNode, attrs.dragTargetNode, dragEvent]); + } + + dragEnd(element, dragEvent) { + if (!this.dragNode) { + return; + } + + const attrs = this.getChartState(); + + d3.select(element).classed(attrs.draggingClass(), false); + + if (!attrs.dragTargetNode) { + this.render(); + return; + } + + if (dragEvent.subject.parent.id === attrs.dragTargetNode.id) { + this.render(); + return; + } + + d3.select(element).remove(); + + const node = attrs.data.find((x) => x.id === dragEvent.subject.id); + attrs.onDrop.apply(element, [attrs.dragNode, attrs.dragTargetNode]); + node.parentId = attrs.dragTargetNode.id; + + + attrs.dragTargetNode = null; + attrs.dragNode = null; + this.render(); + } } \ No newline at end of file