|
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