|
906 | 906 | var ghostNode, nodeCover;
|
907 | 907 | if (!document.querySelector('.ghost-node')) {
|
908 | 908 | ghostNode = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
| 909 | + if (!ghostNode.classList) return; |
909 | 910 | ghostNode.classList.add('ghost-node');
|
910 | 911 | nodeCover = document.createElementNS('http://www.w3.org/2000/svg','rect');
|
911 | 912 | ghostNode.appendChild(nodeCover);
|
|
945 | 946 | ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode);
|
946 | 947 | origEvent.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset);
|
947 | 948 | } else {
|
948 |
| - origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset); |
| 949 | + // IE/Edge do not support this, so only use it if we can |
| 950 | + if (origEvent.dataTransfer.setDragImage) |
| 951 | + origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset); |
949 | 952 | }
|
950 | 953 | },
|
951 | 954 | //
|
952 | 955 | filterAllowedDropNodes: function ($dragged) {
|
953 | 956 | var opts = this.options;
|
954 |
| - var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first'); |
955 |
| - var $dragHier = $dragged.closest('table').find('.node'); |
| 957 | + // what is being dragged? a node, or something within a node? |
| 958 | + var draggingNode = $dragged.closest('[draggable]').hasClass('node'); |
| 959 | + var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first'); // parent node |
| 960 | + var $dragHier = $dragged.closest('table').find('.node'); // this node, and its children |
956 | 961 | this.$chart.data('dragged', $dragged)
|
957 | 962 | .find('.node').each(function (index, node) {
|
958 |
| - if ($dragHier.index(node) === -1) { |
| 963 | + if (!draggingNode || $dragHier.index(node) === -1) { |
959 | 964 | if (opts.dropCriteria) {
|
960 | 965 | if (opts.dropCriteria($dragged, $dragZone, $(node))) {
|
961 | 966 | $(node).addClass('allowedDrop');
|
|
993 | 998 | dropHandler: function (event) {
|
994 | 999 | var $dropZone = $(event.delegateTarget);
|
995 | 1000 | var $dragged = this.$chart.data('dragged');
|
| 1001 | + |
| 1002 | + // Pass on drops which are not nodes (since they are not our doing) |
| 1003 | + if (!$dragged.hasClass('node')) { |
| 1004 | + this.$chart.triggerHandler({ 'type': 'otherdropped.orgchart', 'draggedItem': $dragged, 'dropZone': $dropZone }); |
| 1005 | + return; |
| 1006 | + } |
| 1007 | + |
| 1008 | + if (!$dropZone.hasClass('allowedDrop')) { |
| 1009 | + // We are trying to drop a node into a node which isn't allowed |
| 1010 | + // IE/Edge have a habit of allowing this, so we need our own double-check |
| 1011 | + return; |
| 1012 | + } |
| 1013 | + |
996 | 1014 | var $dragZone = $dragged.closest('.nodes').siblings().eq(0).children();
|
997 | 1015 | var dropEvent = $.Event('nodedrop.orgchart');
|
998 | 1016 | this.$chart.trigger(dropEvent, { 'draggedNode': $dragged, 'dragZone': $dragZone.children(), 'dropZone': $dropZone });
|
|
1039 | 1057 | },
|
1040 | 1058 | //
|
1041 | 1059 | touchstartHandler: function (event) {
|
1042 |
| - console.log("orgChart: touchstart 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", target=" + event.target.innerText); |
1043 |
| - if (this.touchHandled) |
1044 |
| - return; |
1045 |
| - this.touchHandled = true; |
1046 |
| - this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch |
1047 |
| - event.preventDefault(); |
| 1060 | + if (this.touchHandled) |
| 1061 | + return; |
| 1062 | + |
| 1063 | + if (event.touches && event.touches.length > 1) |
| 1064 | + return; |
| 1065 | + |
| 1066 | + this.touchHandled = true; |
| 1067 | + this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch |
| 1068 | + event.preventDefault(); |
1048 | 1069 | },
|
1049 | 1070 | //
|
1050 | 1071 | touchmoveHandler: function (event) {
|
1051 | 1072 | if (!this.touchHandled)
|
1052 | 1073 | return;
|
| 1074 | + |
| 1075 | + if (event.touches && event.touches.length > 1) |
| 1076 | + return; |
| 1077 | + |
1053 | 1078 | event.preventDefault();
|
| 1079 | + |
1054 | 1080 | if (!this.touchMoved) {
|
1055 |
| - var nodeIsSelected = $(this).hasClass('focused'); |
1056 |
| - console.log("orgChart: touchmove 1: " + event.touches.length + " touches, we have not moved, so simulate a drag start", event.touches); |
1057 |
| - // TODO: visualise the start of the drag (as would happen on desktop) |
1058 |
| - this.simulateMouseEvent(event, 'dragstart'); |
| 1081 | + // we do not bother with createGhostNode (dragstart does) since the touch event does not have a dataTransfer property |
| 1082 | + this.filterAllowedDropNodes($(event.currentTarget)); // will also set 'this.$chart.data('dragged')' for us |
| 1083 | + // create an image which can be used to illustrate the drag (our own createGhostNode) |
| 1084 | + this.touchDragImage = this.createDragImage(event, this.$chart.data('dragged')[0]); |
1059 | 1085 | }
|
1060 | 1086 | this.touchMoved = true;
|
1061 |
| - var $touching = $(document.elementFromPoint(event.touches[0].clientX, event.touches[0].clientY)); |
1062 |
| - var $touchingNode = $touching.closest('div.node'); |
1063 | 1087 |
|
1064 |
| - if ($touchingNode.length > 0) { |
1065 |
| - var touchingNodeElement = $touchingNode[0]; |
1066 |
| - // TODO: simulate the dragover visualisation |
1067 |
| - if ($touchingNode.is('.allowedDrop')) { |
1068 |
| - console.log("orgChart: touchmove 2: this node (" + touchingNodeElement.id + ") is allowed to be a drop target"); |
1069 |
| - this.touchTargetNode = touchingNodeElement; |
1070 |
| - } else { |
1071 |
| - console.log("orgChart: touchmove 3: this node (" + touchingNodeElement.id + ") is NOT allowed to be a drop target"); |
1072 |
| - this.touchTargetNode = null; |
| 1088 | + // move our dragimage so it follows our finger |
| 1089 | + this.moveDragImage(event, this.touchDragImage); |
| 1090 | + |
| 1091 | + var $touching = $(document.elementFromPoint(event.touches[0].clientX, event.touches[0].clientY)); |
| 1092 | + var $touchingNodes = $touching.closest('div.node'); |
| 1093 | + if ($touchingNodes.length > 0) { |
| 1094 | + var touchingNodeElement = $touchingNodes[0]; |
| 1095 | + if ($touchingNodes.is('.allowedDrop')) { |
| 1096 | + this.touchTargetNode = touchingNodeElement; |
1073 | 1097 | }
|
1074 |
| - } else { |
1075 |
| - console.log("orgchart: touchmove 4: not touching a node"); |
| 1098 | + else { |
| 1099 | + this.touchTargetNode = null; |
| 1100 | + } |
| 1101 | + } |
| 1102 | + else { |
1076 | 1103 | this.touchTargetNode = null;
|
1077 | 1104 | }
|
1078 | 1105 | },
|
1079 | 1106 | //
|
1080 | 1107 | touchendHandler: function (event) {
|
1081 |
| - console.log("orgChart: touchend 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", " + event.target.innerText + " "); |
1082 | 1108 | if (!this.touchHandled) {
|
1083 |
| - console.log("orgChart: touchend 2: not handled by us, so aborting"); |
1084 | 1109 | return;
|
1085 | 1110 | }
|
| 1111 | + this.destroyDragImage(); |
1086 | 1112 | if (this.touchMoved) {
|
1087 | 1113 | // we've had movement, so this was a 'drag' touch
|
1088 | 1114 | if (this.touchTargetNode) {
|
1089 |
| - console.log("orgChart: touchend 3: moved to a node, so simulating drop"); |
1090 | 1115 | var fakeEventForDropHandler = { delegateTarget: this.touchTargetNode };
|
1091 | 1116 | this.dropHandler(fakeEventForDropHandler);
|
1092 | 1117 | this.touchTargetNode = null;
|
1093 | 1118 | }
|
1094 |
| - console.log("orgChart: touchend 4: simulating dragend"); |
1095 |
| - this.simulateMouseEvent(event, 'dragend'); |
| 1119 | + this.dragendHandler(event); |
1096 | 1120 | }
|
1097 | 1121 | else {
|
1098 |
| - // we did not move, so assume this was a 'press' touch |
1099 |
| - console.log("orgChart: touchend 5: moved, so simulating click"); |
1100 |
| - this.simulateMouseEvent(event, 'click'); |
| 1122 | + // we did not move, so this was a 'press' touch (fake a click) |
| 1123 | + var firstTouch = event.changedTouches[0]; |
| 1124 | + var fakeMouseClickEvent = document.createEvent('MouseEvents'); |
| 1125 | + 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); |
| 1126 | + event.target.dispatchEvent(fakeMouseClickEvent); |
1101 | 1127 | }
|
1102 | 1128 | this.touchHandled = false;
|
1103 | 1129 | },
|
1104 |
| - // simulate a mouse event (so we can fake them on a touch device) |
1105 |
| - simulateMouseEvent: function (event, simulatedType) { |
1106 |
| - // Ignore multi-touch events |
1107 |
| - if (event.originalEvent.touches.length > 1) { |
1108 |
| - return; |
| 1130 | + // |
| 1131 | + createDragImage: function (event, source) { |
| 1132 | + var dragImage = source.cloneNode(true); |
| 1133 | + this.copyStyle(source, dragImage); |
| 1134 | + dragImage.style.top = dragImage.style.left = '-9999px'; |
| 1135 | + var sourceRectangle = source.getBoundingClientRect(); |
| 1136 | + var sourcePoint = this.getTouchPoint(event); |
| 1137 | + this.touchDragImageOffset = { x: sourcePoint.x - sourceRectangle.left, y: sourcePoint.y - sourceRectangle.top }; |
| 1138 | + dragImage.style.opacity = '0.5'; |
| 1139 | + document.body.appendChild(dragImage); |
| 1140 | + return dragImage; |
| 1141 | + }, |
| 1142 | + // |
| 1143 | + destroyDragImage: function () { |
| 1144 | + if (this.touchDragImage && this.touchDragImage.parentElement) |
| 1145 | + this.touchDragImage.parentElement.removeChild(this.touchDragImage); |
| 1146 | + this.touchDragImageOffset = null; |
| 1147 | + this.touchDragImage = null; |
| 1148 | + }, |
| 1149 | + // |
| 1150 | + copyStyle: function (src, dst) { |
| 1151 | + // remove potentially troublesome attributes |
| 1152 | + var badAttributes = ['id', 'class', 'style', 'draggable']; |
| 1153 | + badAttributes.forEach(function (att) { |
| 1154 | + dst.removeAttribute(att); |
| 1155 | + }); |
| 1156 | + // copy canvas content |
| 1157 | + if (src instanceof HTMLCanvasElement) { |
| 1158 | + var cSrc = src, cDst = dst; |
| 1159 | + cDst.width = cSrc.width; |
| 1160 | + cDst.height = cSrc.height; |
| 1161 | + cDst.getContext('2d').drawImage(cSrc, 0, 0); |
| 1162 | + } |
| 1163 | + // copy style (without transitions) |
| 1164 | + var cs = getComputedStyle(src); |
| 1165 | + for (var i = 0; i < cs.length; i++) { |
| 1166 | + var key = cs[i]; |
| 1167 | + if (key.indexOf('transition') < 0) { |
| 1168 | + dst.style[key] = cs[key]; |
| 1169 | + } |
| 1170 | + } |
| 1171 | + dst.style.pointerEvents = 'none'; |
| 1172 | + // and repeat for all children |
| 1173 | + for (var i = 0; i < src.children.length; i++) { |
| 1174 | + this.copyStyle(src.children[i], dst.children[i]); |
| 1175 | + } |
| 1176 | + }, |
| 1177 | + // |
| 1178 | + getTouchPoint: function (event) { |
| 1179 | + if (event && event.touches) { |
| 1180 | + event = event.touches[0]; |
1109 | 1181 | }
|
1110 |
| - var touch = event.originalEvent.changedTouches[0]; |
1111 |
| - var simulatedEvent = document.createEvent('MouseEvents'); |
1112 |
| - simulatedEvent.initMouseEvent( |
1113 |
| - simulatedType, // type |
1114 |
| - true, // bubbles |
1115 |
| - true, // cancelable |
1116 |
| - window, // view |
1117 |
| - 1, // detail |
1118 |
| - touch.screenX, // screenX |
1119 |
| - touch.screenY, // screenY |
1120 |
| - touch.clientX, // clientX |
1121 |
| - touch.clientY, // clientY |
1122 |
| - false, // ctrlKey |
1123 |
| - false, // altKey |
1124 |
| - false, // shiftKey |
1125 |
| - false, // metaKey |
1126 |
| - 0, // button |
1127 |
| - null // relatedTarget |
1128 |
| - ); |
1129 |
| - // Dispatch the simulated event to the target element |
1130 |
| - event.target.dispatchEvent(simulatedEvent); |
| 1182 | + return { |
| 1183 | + x: event.clientX, |
| 1184 | + y: event.clientY |
| 1185 | + }; |
| 1186 | + }, |
| 1187 | + // |
| 1188 | + moveDragImage: function (event, image) { |
| 1189 | + if (!event || !image) |
| 1190 | + return; |
| 1191 | + var orgChartMaster = this; |
| 1192 | + requestAnimationFrame(function () { |
| 1193 | + var pt = orgChartMaster.getTouchPoint(event); |
| 1194 | + var s = image.style; |
| 1195 | + s.position = 'absolute'; |
| 1196 | + s.pointerEvents = 'none'; |
| 1197 | + s.zIndex = '999999'; |
| 1198 | + if (orgChartMaster.touchDragImageOffset) { |
| 1199 | + s.left = Math.round(pt.x - orgChartMaster.touchDragImageOffset.x) + 'px'; |
| 1200 | + s.top = Math.round(pt.y - orgChartMaster.touchDragImageOffset.y) + 'px'; |
| 1201 | + } |
| 1202 | + }); |
1131 | 1203 | },
|
1132 | 1204 | //
|
1133 | 1205 | bindDragDrop: function ($node) {
|
|
1367 | 1439 | }
|
1368 | 1440 | },
|
1369 | 1441 | //
|
| 1442 | + hideDropZones: function () { |
| 1443 | + // Remove all the 'this is a drop zone' indicators |
| 1444 | + var orgChartObj = this; |
| 1445 | + orgChartObj.$chart.find('.allowedDrop') |
| 1446 | + .removeClass('allowedDrop'); |
| 1447 | + }, |
| 1448 | + // |
| 1449 | + showDropZones: function (dragged) { |
| 1450 | + // Highlight all the 'drop zones', and set dragged, so that the drop/enter can work out what happens later |
| 1451 | + // TODO: This assumes all nodes are droppable: it doesn't run the custom isDroppable function - it should! |
| 1452 | + var orgChartObj = this; |
| 1453 | + orgChartObj.$chart.find('.node') |
| 1454 | + .each(function (index, node) { |
| 1455 | + $(node).addClass('allowedDrop'); |
| 1456 | + }); |
| 1457 | + orgChartObj.$chart.data('dragged', $(dragged)); |
| 1458 | + }, |
| 1459 | + // |
| 1460 | + processExternalDrop: function (dropZone, dragged) { |
| 1461 | + // Allow an external drop event to be handled by one of our nodes |
| 1462 | + if (dragged) { |
| 1463 | + this.$chart.data('dragged', $(dragged)); |
| 1464 | + } |
| 1465 | + var droppedOnNode = dropZone.closest('.node'); |
| 1466 | + // would like to just call 'dropZoneHandler', but I can't reach it from here |
| 1467 | + // instead raise a drop event on the node element |
| 1468 | + droppedOnNode.triggerHandler({ 'type': 'drop' }); |
| 1469 | + }, |
| 1470 | + // |
1370 | 1471 | exportPDF: function(canvas, exportFilename){
|
1371 | 1472 | var doc = {};
|
1372 | 1473 | var docWidth = Math.floor(canvas.width);
|
|
0 commit comments