Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add drag-drop #350

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -51,6 +51,7 @@
"dependencies": {
"d3-selection": "3",
"d3-array": "3",
"d3-drag": "3",
"d3-hierarchy": "3",
"d3-zoom": "3",
"d3-shape": "3",
Expand Down
173 changes: 171 additions & 2 deletions src/d3-org-chart.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -17,7 +19,8 @@ const d3 = {
zoom,
zoomIdentity,
linkHorizontal,
flextree
flextree,
drag
}

export class OrgChart {
Expand All @@ -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 */

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}