Skip to content

Commit f27bc27

Browse files
Update jquery.orgchart.js
Enhance support for touch devices, allowing drag-and-drop with 'ghost' nodes appearing as you do so. Support external (to the org chart) drag-and-drop, allowing for example, a 'toolbox' to be created and then nodes can be dragged from there onto the org chart.
1 parent 7673b00 commit f27bc27

File tree

1 file changed

+162
-61
lines changed

1 file changed

+162
-61
lines changed

src/js/jquery.orgchart.js

Lines changed: 162 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,7 @@
898898
var ghostNode, nodeCover;
899899
if (!document.querySelector('.ghost-node')) {
900900
ghostNode = document.createElementNS("http://www.w3.org/2000/svg", "svg");
901+
if (!ghostNode.classList) return;
901902
ghostNode.classList.add('ghost-node');
902903
nodeCover = document.createElementNS('http://www.w3.org/2000/svg','rect');
903904
ghostNode.appendChild(nodeCover);
@@ -936,17 +937,21 @@
936937
ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode);
937938
origEvent.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset);
938939
} else {
939-
origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset);
940+
// IE/Edge do not support this, so only use it if we can
941+
if (origEvent.dataTransfer.setDragImage)
942+
origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset);
940943
}
941944
},
942945
//
943946
filterAllowedDropNodes: function ($dragged) {
944947
var opts = this.options;
945-
var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first');
946-
var $dragHier = $dragged.closest('table').find('.node');
948+
// what is being dragged? a node, or something within a node?
949+
var draggingNode = $dragged.closest('[draggable]').hasClass('node');
950+
var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first'); // parent node
951+
var $dragHier = $dragged.closest('table').find('.node'); // this node, and its children
947952
this.$chart.data('dragged', $dragged)
948953
.find('.node').each(function (index, node) {
949-
if ($dragHier.index(node) === -1) {
954+
if (!draggingNode || $dragHier.index(node) === -1) {
950955
if (opts.dropCriteria) {
951956
if (opts.dropCriteria($dragged, $dragZone, $(node))) {
952957
$(node).addClass('allowedDrop');
@@ -980,6 +985,19 @@
980985
dropHandler: function (event) {
981986
var $dropZone = $(event.delegateTarget);
982987
var $dragged = this.$chart.data('dragged');
988+
989+
// Pass on drops which are not nodes (since they are not our doing)
990+
if (!$dragged.hasClass('node')) {
991+
this.$chart.triggerHandler({ 'type': 'otherdropped.orgchart', 'draggedItem': $dragged, 'dropZone': $dropZone });
992+
return;
993+
}
994+
995+
if (!$dropZone.hasClass('allowedDrop')) {
996+
// We are trying to drop a node into a node which isn't allowed
997+
// IE/Edge have a habit of allowing this, so we need our own double-check
998+
return;
999+
}
1000+
9831001
var $dragZone = $dragged.closest('.nodes').siblings().eq(0).children();
9841002
var dropEvent = $.Event('nodedrop.orgchart');
9851003
this.$chart.trigger(dropEvent, { 'draggedNode': $dragged, 'dragZone': $dragZone.children(), 'dropZone': $dropZone });
@@ -1026,95 +1044,149 @@
10261044
},
10271045
//
10281046
touchstartHandler: function (event) {
1029-
console.log("orgChart: touchstart 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", target=" + event.target.innerText);
1030-
if (this.touchHandled)
1031-
return;
1032-
this.touchHandled = true;
1033-
this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch
1034-
event.preventDefault();
1047+
if (this.touchHandled)
1048+
return;
1049+
1050+
if (event.touches && event.touches.length > 1)
1051+
return;
1052+
1053+
this.touchHandled = true;
1054+
this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch
1055+
event.preventDefault();
10351056
},
10361057
//
10371058
touchmoveHandler: function (event) {
10381059
if (!this.touchHandled)
10391060
return;
1061+
1062+
if (event.touches && event.touches.length > 1)
1063+
return;
1064+
10401065
event.preventDefault();
1066+
10411067
if (!this.touchMoved) {
1042-
var nodeIsSelected = $(this).hasClass('focused');
1043-
console.log("orgChart: touchmove 1: " + event.touches.length + " touches, we have not moved, so simulate a drag start", event.touches);
1044-
// TODO: visualise the start of the drag (as would happen on desktop)
1045-
this.simulateMouseEvent(event, 'dragstart');
1068+
// we do not bother with createGhostNode (dragstart does) since the touch event does not have a dataTransfer property
1069+
this.filterAllowedDropNodes($(event.currentTarget)); // will also set 'this.$chart.data('dragged')' for us
1070+
// create an image which can be used to illustrate the drag (our own createGhostNode)
1071+
this.touchDragImage = this.createDragImage(event, this.$chart.data('dragged')[0]);
10461072
}
10471073
this.touchMoved = true;
1074+
1075+
// move our dragimage so it follows our finger
1076+
this.moveDragImage(event, this.touchDragImage);
1077+
10481078
var $touching = $(document.elementFromPoint(event.touches[0].clientX, event.touches[0].clientY));
1049-
var $touchingNode = $touching.closest('div.node');
1050-
1051-
if ($touchingNode.length > 0) {
1052-
var touchingNodeElement = $touchingNode[0];
1053-
// TODO: simulate the dragover visualisation
1054-
if ($touchingNode.is('.allowedDrop')) {
1055-
console.log("orgChart: touchmove 2: this node (" + touchingNodeElement.id + ") is allowed to be a drop target");
1056-
this.touchTargetNode = touchingNodeElement;
1057-
} else {
1058-
console.log("orgChart: touchmove 3: this node (" + touchingNodeElement.id + ") is NOT allowed to be a drop target");
1059-
this.touchTargetNode = null;
1079+
var $touchingNodes = $touching.closest('div.node');
1080+
if ($touchingNodes.length > 0) {
1081+
var touchingNodeElement = $touchingNodes[0];
1082+
if ($touchingNodes.is('.allowedDrop')) {
1083+
this.touchTargetNode = touchingNodeElement;
10601084
}
1061-
} else {
1062-
console.log("orgchart: touchmove 4: not touching a node");
1085+
else {
1086+
this.touchTargetNode = null;
1087+
}
1088+
}
1089+
else {
10631090
this.touchTargetNode = null;
10641091
}
10651092
},
10661093
//
10671094
touchendHandler: function (event) {
1068-
console.log("orgChart: touchend 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", " + event.target.innerText + " ");
10691095
if (!this.touchHandled) {
1070-
console.log("orgChart: touchend 2: not handled by us, so aborting");
10711096
return;
10721097
}
1098+
this.destroyDragImage();
10731099
if (this.touchMoved) {
10741100
// we've had movement, so this was a 'drag' touch
10751101
if (this.touchTargetNode) {
1076-
console.log("orgChart: touchend 3: moved to a node, so simulating drop");
10771102
var fakeEventForDropHandler = { delegateTarget: this.touchTargetNode };
10781103
this.dropHandler(fakeEventForDropHandler);
10791104
this.touchTargetNode = null;
10801105
}
1081-
console.log("orgChart: touchend 4: simulating dragend");
1082-
this.simulateMouseEvent(event, 'dragend');
1106+
this.dragendHandler(event);
10831107
}
10841108
else {
1085-
// we did not move, so assume this was a 'press' touch
1086-
console.log("orgChart: touchend 5: moved, so simulating click");
1087-
this.simulateMouseEvent(event, 'click');
1109+
// we did not move, so this was a 'press' touch (fake a click)
1110+
var firstTouch = event.changedTouches[0];
1111+
var fakeMouseClickEvent = document.createEvent('MouseEvents');
1112+
fakeMouseClickEvent.initMouseEvent('click', true, true, window, 1, firstTouch.screenX, firstTouch.screenY, firstTouch.clientX, firstTouch.clientY, event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, 0, null);
1113+
event.target.dispatchEvent(fakeMouseClickEvent);
10881114
}
10891115
this.touchHandled = false;
10901116
},
1091-
// simulate a mouse event (so we can fake them on a touch device)
1092-
simulateMouseEvent: function (event, simulatedType) {
1093-
// Ignore multi-touch events
1094-
if (event.originalEvent.touches.length > 1) {
1095-
return;
1117+
//
1118+
createDragImage: function (event, source) {
1119+
var dragImage = source.cloneNode(true);
1120+
this.copyStyle(source, dragImage);
1121+
dragImage.style.top = dragImage.style.left = '-9999px';
1122+
var sourceRectangle = source.getBoundingClientRect();
1123+
var sourcePoint = this.getTouchPoint(event);
1124+
this.touchDragImageOffset = { x: sourcePoint.x - sourceRectangle.left, y: sourcePoint.y - sourceRectangle.top };
1125+
dragImage.style.opacity = '0.5';
1126+
document.body.appendChild(dragImage);
1127+
return dragImage;
1128+
},
1129+
//
1130+
destroyDragImage: function () {
1131+
if (this.touchDragImage && this.touchDragImage.parentElement)
1132+
this.touchDragImage.parentElement.removeChild(this.touchDragImage);
1133+
this.touchDragImageOffset = null;
1134+
this.touchDragImage = null;
1135+
},
1136+
//
1137+
copyStyle: function (src, dst) {
1138+
// remove potentially troublesome attributes
1139+
var badAttributes = ['id', 'class', 'style', 'draggable'];
1140+
badAttributes.forEach(function (att) {
1141+
dst.removeAttribute(att);
1142+
});
1143+
// copy canvas content
1144+
if (src instanceof HTMLCanvasElement) {
1145+
var cSrc = src, cDst = dst;
1146+
cDst.width = cSrc.width;
1147+
cDst.height = cSrc.height;
1148+
cDst.getContext('2d').drawImage(cSrc, 0, 0);
1149+
}
1150+
// copy style (without transitions)
1151+
var cs = getComputedStyle(src);
1152+
for (var i = 0; i < cs.length; i++) {
1153+
var key = cs[i];
1154+
if (key.indexOf('transition') < 0) {
1155+
dst.style[key] = cs[key];
1156+
}
10961157
}
1097-
var touch = event.originalEvent.changedTouches[0];
1098-
var simulatedEvent = document.createEvent('MouseEvents');
1099-
simulatedEvent.initMouseEvent(
1100-
simulatedType, // type
1101-
true, // bubbles
1102-
true, // cancelable
1103-
window, // view
1104-
1, // detail
1105-
touch.screenX, // screenX
1106-
touch.screenY, // screenY
1107-
touch.clientX, // clientX
1108-
touch.clientY, // clientY
1109-
false, // ctrlKey
1110-
false, // altKey
1111-
false, // shiftKey
1112-
false, // metaKey
1113-
0, // button
1114-
null // relatedTarget
1115-
);
1116-
// Dispatch the simulated event to the target element
1117-
event.target.dispatchEvent(simulatedEvent);
1158+
dst.style.pointerEvents = 'none';
1159+
// and repeat for all children
1160+
for (var i = 0; i < src.children.length; i++) {
1161+
this.copyStyle(src.children[i], dst.children[i]);
1162+
}
1163+
},
1164+
//
1165+
getTouchPoint: function (event) {
1166+
if (event && event.touches) {
1167+
event = event.touches[0];
1168+
}
1169+
return {
1170+
x: event.clientX,
1171+
y: event.clientY
1172+
};
1173+
},
1174+
//
1175+
moveDragImage: function (event, image) {
1176+
if (!event || !image)
1177+
return;
1178+
var orgChartMaster = this;
1179+
requestAnimationFrame(function () {
1180+
var pt = orgChartMaster.getTouchPoint(event);
1181+
var s = image.style;
1182+
s.position = 'absolute';
1183+
s.pointerEvents = 'none';
1184+
s.zIndex = '999999';
1185+
if (orgChartMaster.touchDragImageOffset) {
1186+
s.left = Math.round(pt.x - orgChartMaster.touchDragImageOffset.x) + 'px';
1187+
s.top = Math.round(pt.y - orgChartMaster.touchDragImageOffset.y) + 'px';
1188+
}
1189+
});
11181190
},
11191191
//
11201192
bindDragDrop: function ($node) {
@@ -1350,6 +1422,35 @@
13501422
}
13511423
},
13521424
//
1425+
hideDropZones: function () {
1426+
// Remove all the 'this is a drop zone' indicators
1427+
var orgChartObj = this;
1428+
orgChartObj.$chart.find('.allowedDrop')
1429+
.removeClass('allowedDrop');
1430+
},
1431+
//
1432+
showDropZones: function (dragged) {
1433+
// Highlight all the 'drop zones', and set dragged, so that the drop/enter can work out what happens later
1434+
// TODO: This assumes all nodes are droppable: it doesn't run the custom isDroppable function - it should!
1435+
var orgChartObj = this;
1436+
orgChartObj.$chart.find('.node')
1437+
.each(function (index, node) {
1438+
$(node).addClass('allowedDrop');
1439+
});
1440+
orgChartObj.$chart.data('dragged', $(dragged));
1441+
},
1442+
//
1443+
processExternalDrop: function (dropZone, dragged) {
1444+
// Allow an external drop event to be handled by one of our nodes
1445+
if (dragged) {
1446+
this.$chart.data('dragged', $(dragged));
1447+
}
1448+
var droppedOnNode = dropZone.closest('.node');
1449+
// would like to just call 'dropZoneHandler', but I can't reach it from here
1450+
// instead raise a drop event on the node element
1451+
droppedOnNode.triggerHandler({ 'type': 'drop' });
1452+
},
1453+
//
13531454
export: function (exportFilename, exportFileextension) {
13541455
var that = this;
13551456
exportFilename = (typeof exportFilename !== 'undefined') ? exportFilename : this.options.exportFilename;

0 commit comments

Comments
 (0)