');
768 | this.element(element);
769 | this.redraw();
770 | dom.append(parentElement, element);
771 |
772 | return;
773 | }
774 |
775 | // remove element
776 | if (!parentElement && element) {
777 | dom.remove(element);
778 | this.element(null);
779 | this.cache({});
780 |
781 | return;
782 | }
783 |
784 | var cache = this.cache();
785 |
786 | // update path container element
787 | var pathContainerStyle = this.pathContainerStyle();
788 | var pathContainerElement = dom.child(element, 0);
789 |
790 | dom.css(pathContainerElement, helper.diffObj(pathContainerStyle, cache.pathContainerElementStyle));
791 | cache.pathContainerElementStyle = contentStyle;
792 |
793 | // update line element
794 | var lineAttributes = this.lineAttributes();
795 | var lineElement = dom.child(pathContainerElement, 0);
796 |
797 | dom.attr(lineElement, helper.diffObj(lineAttributes, cache.lineAttributes));
798 | cache.lineAttributes = lineAttributes;
799 |
800 | // update arrow element
801 | var arrowAttributes = this.arrowAttributes();
802 | var arrowElement = dom.child(pathContainerElement, 1);
803 |
804 | dom.attr(arrowElement, helper.diffObj(arrowAttributes, cache.arrowAttributes));
805 | cache.arrowAttributes = arrowAttributes;
806 |
807 | // update content element
808 | var content = this.content();
809 | var contentStyle = this.contentStyle();
810 | var contentElement = dom.child(element, 1);
811 |
812 | if (content !== cache.content) {
813 | var contentType = this.contentType();
814 |
815 | if (contentType === helper.CONTENT_TYPE_TEXT)
816 | dom.text(contentElement, content);
817 | else if (contentType === helper.CONTENT_TYPE_HTML)
818 | dom.html(contentElement, content);
819 |
820 | cache.content = content;
821 | }
822 |
823 | dom.css(contentElement, helper.diffObj(contentStyle, cache.contentStyle));
824 | cache.contentStyle = contentStyle;
825 |
826 | // update container element
827 | var style = this.style();
828 |
829 | dom.css(element, helper.diffObj(style, cache.style));
830 | cache.style = style;
831 | };
832 |
833 | var Connector = helper.inherits(function(props) {
834 | this.x = this.prop(props.x, 0, helper.toNumber);
835 | this.y = this.prop(props.y, 0, helper.toNumber);
836 | this.color = this.prop(Connector.COLOR_UNCONNECTED);
837 | this.zIndex = this.prop('auto');
838 | this.element = this.prop(null);
839 | this.parentElement = this.prop(null);
840 | this.cache = this.prop({});
841 | this.relations = this.prop([]);
842 | }, Component);
843 |
844 | Connector.prototype.r = function() {
845 | return 16;
846 | };
847 |
848 | Connector.prototype.contains = function(x, y, tolerance) {
849 | var dx = x - this.x();
850 | var dy = y - this.y();
851 | var r = this.r() + tolerance;
852 |
853 | return (dx * dx + dy * dy <= r * r);
854 | };
855 |
856 | Connector.prototype.style = function() {
857 | var r = this.r();
858 | var x = this.x() - r;
859 | var y = this.y() - r;
860 | var translate = 'translate(' + x + 'px, ' + y + 'px)';
861 |
862 | return {
863 | backgroundColor: this.color(),
864 | border: '2px solid lightgray',
865 | borderRadius: '50%',
866 | boxSizing: 'border-box',
867 | height: r * 2 + 'px',
868 | msTransform: translate,
869 | opacity: 0.6,
870 | pointerEvents: 'none',
871 | position: 'absolute',
872 | transform: translate,
873 | webkitTransform: translate,
874 | width: r * 2 + 'px',
875 | zIndex: this.zIndex()
876 | };
877 | };
878 |
879 | Connector.prototype.redraw = function() {
880 | var element = this.element();
881 | var parentElement = this.parentElement();
882 |
883 | if (!parentElement && !element)
884 | return;
885 |
886 | // add element
887 | if (parentElement && !element) {
888 | element = dom.el('
');
889 | this.element(element);
890 | this.redraw();
891 | dom.append(parentElement, element);
892 |
893 | return;
894 | }
895 |
896 | // remove element
897 | if (!parentElement && element) {
898 | dom.remove(element);
899 | this.element(null);
900 |
901 | return;
902 | }
903 |
904 | var cache = this.cache();
905 |
906 | // update element
907 | var style = this.style();
908 |
909 | dom.css(element, helper.diffObj(style, cache.style));
910 | cache.style = style;
911 | };
912 |
913 | Connector.COLOR_CONNECTED = 'lightgreen';
914 | Connector.COLOR_UNCONNECTED = 'pink';
915 |
916 | var Relation = function() {};
917 |
918 | Relation.prototype.prop = function(initialValue) {
919 | var cache = initialValue;
920 |
921 | return function(value) {
922 | if (typeof value === 'undefined')
923 | return cache;
924 |
925 | cache = value;
926 | };
927 | };
928 |
929 | Relation.prototype.update = function() {};
930 |
931 | var Triple = helper.inherits(function(props) {
932 | this.link = this.prop(props.link);
933 | this.sourceNode = this.prop(props.sourceNode || null);
934 | this.targetNode = this.prop(props.targetNode || null);
935 | this.skipNextUpdate = this.prop(false);
936 | this.nodePositionsCache = this.prop({});
937 | }, Relation);
938 |
939 | Triple.prototype.update = function(changedComponent) {
940 | if (this.skipNextUpdate()) {
941 | this.skipNextUpdate(false);
942 | return;
943 | }
944 |
945 | var link = this.link();
946 | var sourceNode = this.sourceNode();
947 | var targetNode = this.targetNode();
948 |
949 | if (changedComponent instanceof Node)
950 | this.updateNode(link, sourceNode, targetNode, changedComponent);
951 | else if (changedComponent instanceof Link)
952 | this.updateLink(link, sourceNode, targetNode);
953 | };
954 |
955 | Triple.prototype.updateNode = function(link, sourceNode, targetNode, changedNode) {
956 | if (sourceNode && targetNode)
957 | this.rotateLink(link, sourceNode, targetNode, changedNode);
958 | else
959 | this.shiftLink(link, sourceNode, targetNode, changedNode);
960 |
961 | this.updateNodePositionsCache();
962 | };
963 |
964 | Triple.prototype.rotateLink = function(link, sourceNode, targetNode, changedNode) {
965 | var cache = this.nodePositionsCache();
966 |
967 | var sncx = cache.sncx;
968 | var sncy = cache.sncy;
969 | var tncx = cache.tncx;
970 | var tncy = cache.tncy;
971 |
972 | var lcx = link.cx();
973 | var lcy = link.cy();
974 |
975 | var ts_dx = tncx - sncx;
976 | var ts_dy = tncy - sncy;
977 | var cs_dx = lcx - sncx;
978 | var cs_dy = lcy - sncy;
979 |
980 | var ts_rad0 = Math.atan2(ts_dy, ts_dx);
981 | var cs_rad0 = Math.atan2(cs_dy, cs_dx);
982 |
983 | // changed node position
984 | if (changedNode === sourceNode) {
985 | sncx = sourceNode.cx();
986 | sncy = sourceNode.cy();
987 | } else if (changedNode === targetNode) {
988 | tncx = targetNode.cx();
989 | tncy = targetNode.cy();
990 | }
991 |
992 | // center positions of two nodes are equal
993 | if (cs_rad0 === 0) {
994 | link.cx((sncx + tncx) / 2);
995 | link.cy((sncy + tncy) / 2);
996 |
997 | return;
998 | }
999 |
1000 | var ts_d0 = Math.sqrt(ts_dx * ts_dx + ts_dy * ts_dy);
1001 | var cs_d0 = Math.sqrt(cs_dx * cs_dx + cs_dy * cs_dy);
1002 |
1003 | var ts_cs_rad = ts_rad0 - cs_rad0;
1004 |
1005 | ts_dx = tncx - sncx;
1006 | ts_dy = tncy - sncy;
1007 |
1008 | var ts_rad1 = Math.atan2(ts_dy, ts_dx);
1009 | var cs_rad1 = ts_rad1 - ts_cs_rad;
1010 |
1011 | var ts_d1 = Math.sqrt(ts_dx * ts_dx + ts_dy * ts_dy);
1012 | var d_rate = (ts_d0 !== 0) ? ts_d1 / ts_d0 : 1;
1013 | var cs_d1 = cs_d0 * d_rate;
1014 |
1015 | lcx = sncx + cs_d1 * Math.cos(cs_rad1);
1016 | lcy = sncy + cs_d1 * Math.sin(cs_rad1);
1017 |
1018 | link.cx(lcx);
1019 | link.cy(lcy);
1020 | };
1021 |
1022 | Triple.prototype.shiftLink = function(link, sourceNode, targetNode, changedNode) {
1023 | var cache = this.nodePositionsCache();
1024 |
1025 | var ncx = changedNode.cx();
1026 | var ncy = changedNode.cy();
1027 |
1028 | if (changedNode === sourceNode) {
1029 | link.targetX(link.targetX() + (ncx - cache.sncx));
1030 | link.targetY(link.targetY() + (ncy - cache.sncy));
1031 | } else if (changedNode === targetNode) {
1032 | link.sourceX(link.sourceX() + (ncx - cache.tncx));
1033 | link.sourceY(link.sourceY() + (ncy - cache.tncy));
1034 | }
1035 | };
1036 |
1037 | Triple.prototype.updateLink = function(link, sourceNode, targetNode) {
1038 | var lx, ly, p;
1039 |
1040 | if (sourceNode) {
1041 | // connect link to source node
1042 | lx = targetNode ? link.cx() : link.targetX();
1043 | ly = targetNode ? link.cy() : link.targetY();
1044 | p = this.connectedPoint(sourceNode, lx, ly);
1045 | link.sourceX(p.x);
1046 | link.sourceY(p.y);
1047 | }
1048 |
1049 | if (targetNode) {
1050 | // connect link to target node
1051 | lx = sourceNode ? link.cx() : link.sourceX();
1052 | ly = sourceNode ? link.cy() : link.sourceY();
1053 | p = this.connectedPoint(targetNode, lx, ly);
1054 | link.targetX(p.x);
1055 | link.targetY(p.y);
1056 | }
1057 |
1058 | if (!sourceNode || !targetNode) {
1059 | // link content moves to midpoint
1060 | link.cx((link.sourceX() + link.targetX()) / 2);
1061 | link.cy((link.sourceY() + link.targetY()) / 2);
1062 | }
1063 | };
1064 |
1065 | Triple.prototype.updateLinkAngle = function(radians) {
1066 | var link = this.link();
1067 | var sourceNode = this.sourceNode();
1068 | var targetNode = this.targetNode();
1069 |
1070 | var ldx = link.targetX() - link.sourceX();
1071 | var ldy = link.targetY() - link.sourceY();
1072 | var d = Math.sqrt(ldx * ldx + ldy * ldy);
1073 |
1074 | var connectedNode = sourceNode || targetNode;
1075 | var cx = connectedNode.cx();
1076 | var cy = connectedNode.cy();
1077 | var lx = cx + d * Math.cos(radians);
1078 | var ly = cy + d * Math.sin(radians);
1079 | var p = this.connectedPoint(connectedNode, lx, ly);
1080 |
1081 | if (connectedNode === sourceNode)
1082 | link.straighten(p.x, p.y, lx + p.x - cx, ly + p.y - cy);
1083 | else if (connectedNode === targetNode)
1084 | link.straighten(lx + p.x - cx, ly + p.y - cy, p.x, p.y);
1085 | };
1086 |
1087 | Triple.prototype.updateNodePositionsCache = function() {
1088 | var sourceNode = this.sourceNode();
1089 | var targetNode = this.targetNode();
1090 | var cache = this.nodePositionsCache();
1091 |
1092 | if (sourceNode) {
1093 | cache.sncx = sourceNode.cx();
1094 | cache.sncy = sourceNode.cy();
1095 | }
1096 |
1097 | if (targetNode) {
1098 | cache.tncx = targetNode.cx();
1099 | cache.tncy = targetNode.cy();
1100 | }
1101 | };
1102 |
1103 | Triple.prototype.connectedPoint = function(node, lx, ly) {
1104 | var nx = node.x();
1105 | var ny = node.y();
1106 | var nwidth = node.width();
1107 | var nheight = node.height();
1108 | var ncx = node.cx();
1109 | var ncy = node.cy();
1110 |
1111 | var alpha = Math.atan2(ly - ncy, lx - ncx);
1112 | var beta = Math.PI / 2 - alpha;
1113 | var t = Math.atan2(nheight, nwidth);
1114 |
1115 | var x, y;
1116 |
1117 | // left edge
1118 | if (alpha < t - Math.PI || alpha > Math.PI - t) {
1119 | x = nx;
1120 | y = ncy - nwidth * Math.tan(alpha) / 2;
1121 | }
1122 | // top edge
1123 | else if (alpha < -t) {
1124 | x = ncx - nheight * Math.tan(beta) / 2;
1125 | y = ny;
1126 | }
1127 | // right edge
1128 | else if (alpha < t) {
1129 | x = nx + nwidth;
1130 | y = ncy + nwidth * Math.tan(alpha) / 2;
1131 | }
1132 | // bottom edge
1133 | else {
1134 | x = ncx + nheight * Math.tan(beta) / 2;
1135 | y = ny + nheight;
1136 | }
1137 |
1138 | var x0, y0, l, ex, ey;
1139 | var r = node.borderRadius();
1140 | var atCorner = false;
1141 |
1142 | // top-left corner
1143 | if (x < nx + r && y < ny + r) {
1144 | x0 = nx + r;
1145 | y0 = ny + r;
1146 | atCorner = true;
1147 | }
1148 | // top-right corner
1149 | else if (x > nx + nwidth - r && y < ny + r) {
1150 | x0 = nx + nwidth - r;
1151 | y0 = ny + r;
1152 | atCorner = true;
1153 | }
1154 | // bottom-left corner
1155 | else if (x < nx + r && y > ny + nheight - r) {
1156 | x0 = nx + r;
1157 | y0 = ny + nheight - r;
1158 | atCorner = true;
1159 | }
1160 | // bottom-right corner
1161 | else if (x > nx + nwidth - r && y > ny + nheight - r) {
1162 | x0 = nx + nwidth - r;
1163 | y0 = ny + nheight - r;
1164 | atCorner = true;
1165 | }
1166 |
1167 | if (atCorner) {
1168 | l = Math.sqrt((x0 - x) * (x0 - x) + (y0 - y) * (y0 - y));
1169 | ex = (x0 - x) / l;
1170 | ey = (y0 - y) / l;
1171 | x = x0 - r * ex;
1172 | y = y0 - r * ey;
1173 | }
1174 |
1175 | return {
1176 | x: x,
1177 | y: y
1178 | };
1179 | };
1180 |
1181 | var LinkConnectorRelation = helper.inherits(function(props) {
1182 | this.type = this.prop(props.type);
1183 | this.link = this.prop(props.link);
1184 | this.connector = this.prop(props.connector);
1185 | }, Relation);
1186 |
1187 | LinkConnectorRelation.prototype.isConnected = function(isConnected) {
1188 | var color = isConnected ? Connector.COLOR_CONNECTED : Connector.COLOR_UNCONNECTED;
1189 | this.connector().color(color);
1190 | };
1191 |
1192 | LinkConnectorRelation.prototype.update = function(changedComponent) {
1193 | var type = this.type();
1194 | var link = this.link();
1195 | var connector = this.connector();
1196 |
1197 | if (changedComponent === link) {
1198 | connector.x(link[type + 'X']());
1199 | connector.y(link[type + 'Y']());
1200 | }
1201 | };
1202 |
1203 | var ComponentList = helper.inherits(function() {
1204 | ComponentList.super_.call(this);
1205 | }, helper.List);
1206 |
1207 | ComponentList.prototype.toFront = function(component) {
1208 | var data = this.data;
1209 | var index = data.indexOf(component);
1210 |
1211 | if (index === -1)
1212 | return;
1213 |
1214 | data.splice(index, 1);
1215 | data.push(component);
1216 | };
1217 |
1218 | ComponentList.prototype.fromPoint = function(ctor, x, y) {
1219 | var data = this.data;
1220 | var closeComponent = null;
1221 |
1222 | for (var i = data.length - 1; i >= 0; i--) {
1223 | var component = data[i];
1224 |
1225 | if (!(component instanceof ctor))
1226 | continue;
1227 |
1228 | if (component.contains(x, y, 0))
1229 | return component;
1230 |
1231 | if (!closeComponent && component.contains(x, y, 8))
1232 | closeComponent = component;
1233 | }
1234 |
1235 | return closeComponent;
1236 | };
1237 |
1238 | var DisabledConnectorList = helper.inherits(function() {
1239 | DisabledConnectorList.super_.call(this);
1240 | }, helper.List);
1241 |
1242 | DisabledConnectorList.prototype.add = function(type, link) {
1243 | DisabledConnectorList.super_.prototype.add.call(this, {
1244 | type: type,
1245 | link: link
1246 | });
1247 | };
1248 |
1249 | DisabledConnectorList.prototype.remove = function(type, link) {
1250 | DisabledConnectorList.super_.prototype.remove.call(this, {
1251 | type: type,
1252 | link: link
1253 | });
1254 | };
1255 |
1256 | DisabledConnectorList.prototype.contains = function(type, link) {
1257 | return DisabledConnectorList.super_.prototype.contains.call(this, {
1258 | type: type,
1259 | link: link
1260 | });
1261 | };
1262 |
1263 | DisabledConnectorList.prototype.equal = function(a, b) {
1264 | return a.type === b.type && a.link === b.link;
1265 | };
1266 |
1267 | var Cmap = helper.inherits(function(rootElement) {
1268 | this.componentList = this.prop(new ComponentList());
1269 | this.disabledConnectorList = this.prop(new DisabledConnectorList());
1270 | this.dragDisabledComponentList = this.prop(new ComponentList());
1271 | this.element = this.prop(null);
1272 | this.rootElement = this.prop(rootElement || null);
1273 | this.retainerElement = this.prop(null);
1274 | this.dragContext = this.prop({});
1275 |
1276 | this.markDirty();
1277 | }, Component);
1278 |
1279 | Cmap.prototype.add = function(component) {
1280 | component.parentElement(this.element());
1281 | this.componentList().add(component);
1282 | this.updateZIndex();
1283 | };
1284 |
1285 | Cmap.prototype.remove = function(component) {
1286 | component.parentElement(null);
1287 |
1288 | if (component instanceof Link)
1289 | this.hideConnectors(component);
1290 |
1291 | this.disconnect(component);
1292 | this.componentList().remove(component);
1293 | this.updateZIndex();
1294 | };
1295 |
1296 | Cmap.prototype.toFront = function(component) {
1297 | this.componentList().toFront(component);
1298 | this.updateZIndex();
1299 | };
1300 |
1301 | Cmap.prototype.updateZIndex = function() {
1302 | this.componentList().toArray().forEach(function(component, index) {
1303 | if (component instanceof Connector)
1304 | return;
1305 |
1306 | // update z-index of node/link
1307 | var zIndex = index * 10;
1308 | component.zIndex(zIndex);
1309 |
1310 | if (!(component instanceof Link))
1311 | return;
1312 |
1313 | // update connector z-index of link
1314 | helper.eachInstance(component.relations(), LinkConnectorRelation, function(relation, index) {
1315 | relation.connector().zIndex(zIndex + index + 1);
1316 | });
1317 | });
1318 | };
1319 |
1320 | Cmap.prototype.connect = function(type, node, link) {
1321 | var linkRelations = link.relations();
1322 | var triple = helper.firstInstance(linkRelations, Triple);
1323 | var nodeKey = type + 'Node';
1324 |
1325 | if (triple && triple[nodeKey]())
1326 | throw new Error('Already connected');
1327 |
1328 | var anotherType = Cmap.anotherConnectionType(type);
1329 | var anotherSideNode = triple ? triple[anotherType + 'Node']() : null;
1330 |
1331 | if (anotherSideNode === node)
1332 | throw new Error('Already connected to the ' + anotherType + ' of the link');
1333 |
1334 | if (triple) {
1335 | triple[nodeKey](node);
1336 | } else {
1337 | var tripleProps = {};
1338 | tripleProps.link = link;
1339 | tripleProps[nodeKey] = node;
1340 | triple = new Triple(tripleProps);
1341 |
1342 | // add triple to the beginning of link relations to be ahead of link-connector relation
1343 | // connector position won't be updated before triple update
1344 | linkRelations.unshift(triple);
1345 | }
1346 |
1347 | // add triple to node
1348 | node.relations().push(triple);
1349 | triple.updateNodePositionsCache();
1350 |
1351 | // update connectors of link
1352 | helper.eachInstance(linkRelations, LinkConnectorRelation, function(relation) {
1353 | if (relation.type() === type)
1354 | relation.isConnected(true);
1355 | });
1356 |
1357 | // link content moves to midpoint of connected nodes
1358 | if (anotherSideNode) {
1359 | link.cx((node.cx() + anotherSideNode.cx()) / 2);
1360 | link.cy((node.cy() + anotherSideNode.cy()) / 2);
1361 | }
1362 |
1363 | // do not need to mark node dirty (stay unchanged)
1364 | link.markDirty();
1365 | };
1366 |
1367 | Cmap.prototype.disconnect = function(type, node, link) {
1368 | if (type instanceof Component) {
1369 | var component = type;
1370 | var relations = component.relations().slice();
1371 |
1372 | // disconnect all connections of component
1373 | helper.eachInstance(relations, Triple, function(triple) {
1374 | var link = triple.link();
1375 | var sourceNode = triple.sourceNode();
1376 | var targetNode = triple.targetNode();
1377 |
1378 | if (sourceNode && (component === link || component === sourceNode))
1379 | this.disconnect(Cmap.CONNECTION_TYPE_SOURCE, sourceNode, link);
1380 |
1381 | if (targetNode && (component === link || component === targetNode))
1382 | this.disconnect(Cmap.CONNECTION_TYPE_TARGET, targetNode, link);
1383 | }.bind(this));
1384 |
1385 | return;
1386 | }
1387 |
1388 | var linkRelations = link.relations();
1389 | var triple = helper.firstInstance(linkRelations, Triple);
1390 | var nodeKey = type + 'Node';
1391 |
1392 | if (!triple || triple[nodeKey]() !== node)
1393 | throw new Error('Not connected');
1394 |
1395 | triple[nodeKey](null);
1396 |
1397 | // remove triple from node
1398 | var nodeRelations = node.relations();
1399 | nodeRelations.splice(nodeRelations.indexOf(triple), 1);
1400 |
1401 | // remove triple from link
1402 | if (!triple.sourceNode() && !triple.targetNode())
1403 | linkRelations.splice(linkRelations.indexOf(triple), 1);
1404 |
1405 | // update connectors of link
1406 | helper.eachInstance(linkRelations, LinkConnectorRelation, function(relation) {
1407 | if (relation.type() === type)
1408 | relation.isConnected(false);
1409 | });
1410 |
1411 | // do not need to mark node dirty (stay unchanged)
1412 | link.markDirty();
1413 | };
1414 |
1415 | Cmap.prototype.connectedNode = function(type, link) {
1416 | var triple = helper.firstInstance(link.relations(), Triple);
1417 |
1418 | if (!triple)
1419 | return null;
1420 |
1421 | return triple[type + 'Node']();
1422 | };
1423 |
1424 | Cmap.prototype.showConnector = function(type, link) {
1425 | if (this.connectorVisible(type, link))
1426 | return;
1427 |
1428 | var disabledConnectorList = this.disabledConnectorList();
1429 | var connectorDisabled = disabledConnectorList.contains(type, link);
1430 |
1431 | if (!connectorDisabled)
1432 | this.addConnector(type, link);
1433 | };
1434 |
1435 | Cmap.prototype.connectorVisible = function(type, link) {
1436 | return link.relations().some(function(relation) {
1437 | return relation instanceof LinkConnectorRelation && relation.type() === type;
1438 | });
1439 | };
1440 |
1441 | Cmap.prototype.addConnector = function(type, link) {
1442 | var connector = new Connector({
1443 | x: link[type + 'X'](),
1444 | y: link[type + 'Y']()
1445 | });
1446 |
1447 | var linkConnectorRelation = new LinkConnectorRelation({
1448 | type: type,
1449 | link: link,
1450 | connector: connector
1451 | });
1452 |
1453 | var linkRelations = link.relations();
1454 | var triple = helper.firstInstance(linkRelations, Triple);
1455 | var isConnected = (triple && !!triple[type + 'Node']());
1456 |
1457 | linkConnectorRelation.isConnected(isConnected);
1458 | linkRelations.push(linkConnectorRelation);
1459 | connector.relations().push(linkConnectorRelation);
1460 |
1461 | this.add(connector);
1462 | };
1463 |
1464 | Cmap.prototype.hideConnector = function(type, link) {
1465 | var linkRelations = link.relations();
1466 |
1467 | for (var i = linkRelations.length - 1; i >= 0; i--) {
1468 | var relation = linkRelations[i];
1469 |
1470 | if (!(relation instanceof LinkConnectorRelation) || relation.type() !== type)
1471 | continue;
1472 |
1473 | // remove connector component
1474 | this.remove(relation.connector());
1475 |
1476 | // remove link-connector relation from link
1477 | linkRelations.splice(i, 1);
1478 |
1479 | break;
1480 | }
1481 | };
1482 |
1483 | Cmap.prototype.showConnectors = function(link) {
1484 | this.showConnector(Cmap.CONNECTION_TYPE_SOURCE, link);
1485 | this.showConnector(Cmap.CONNECTION_TYPE_TARGET, link);
1486 | };
1487 |
1488 | Cmap.prototype.hideConnectors = function(link) {
1489 | this.hideConnector(Cmap.CONNECTION_TYPE_SOURCE, link);
1490 | this.hideConnector(Cmap.CONNECTION_TYPE_TARGET, link);
1491 | };
1492 |
1493 | Cmap.prototype.hideAllConnectors = function() {
1494 | this.componentList().toArray().forEach(function(component) {
1495 | if (component instanceof Link)
1496 | this.hideConnectors(component);
1497 | }.bind(this));
1498 | };
1499 |
1500 | Cmap.prototype.enableConnector = function(type, link) {
1501 | this.disabledConnectorList().remove(type, link);
1502 | };
1503 |
1504 | Cmap.prototype.disableConnector = function(type, link) {
1505 | // remove showing connector
1506 | this.hideConnector(type, link);
1507 |
1508 | this.disabledConnectorList().add(type, link);
1509 | };
1510 |
1511 | Cmap.prototype.connectorEnabled = function(type, link) {
1512 | return !this.disabledConnectorList().contains(type, link);
1513 | };
1514 |
1515 | Cmap.prototype.enableDrag = function(component) {
1516 | this.dragDisabledComponentList().remove(component);
1517 | };
1518 |
1519 | Cmap.prototype.disableDrag = function(component) {
1520 | this.dragDisabledComponentList().add(component);
1521 | };
1522 |
1523 | Cmap.prototype.dragEnabled = function(component) {
1524 | return !this.dragDisabledComponentList().contains(component);
1525 | };
1526 |
1527 | Cmap.prototype.onstart = function(x, y, event) {
1528 | var context = this.dragContext();
1529 |
1530 | var component = this.componentList().fromPoint(Component, x, y);
1531 | context.component = component;
1532 |
1533 | if (!(component instanceof Connector))
1534 | this.hideAllConnectors();
1535 |
1536 | if (!component)
1537 | return;
1538 |
1539 | var draggable = !this.dragDisabledComponentList().contains(component);
1540 | context.draggable = draggable;
1541 |
1542 | if (!draggable)
1543 | return;
1544 |
1545 | dom.cancel(event);
1546 |
1547 | this.toFront(component);
1548 |
1549 | if (component instanceof Node) {
1550 | context.x = component.x();
1551 | context.y = component.y();
1552 | } else if (component instanceof Link) {
1553 | context.cx = component.cx();
1554 | context.cy = component.cy();
1555 | context.sourceX = component.sourceX();
1556 | context.sourceY = component.sourceY();
1557 | context.targetX = component.targetX();
1558 | context.targetY = component.targetY();
1559 | context.triple = helper.firstInstance(component.relations(), Triple);
1560 |
1561 | this.showConnectors(component);
1562 | } else if (component instanceof Connector) {
1563 | var linkConnectorRelation = helper.firstInstance(component.relations(), LinkConnectorRelation);
1564 |
1565 | context.x = x;
1566 | context.y = y;
1567 | context.type = linkConnectorRelation.type();
1568 | context.link = linkConnectorRelation.link();
1569 | }
1570 |
1571 | this.fixScrollSize();
1572 | };
1573 |
1574 | Cmap.prototype.onmove = function(dx, dy, event) {
1575 | var context = this.dragContext();
1576 |
1577 | var component = context.component;
1578 |
1579 | if (!component)
1580 | return;
1581 |
1582 | if (!context.draggable)
1583 | return;
1584 |
1585 | if (component instanceof Node) {
1586 | component.x(context.x + dx);
1587 | component.y(context.y + dy);
1588 | } else if (component instanceof Link) {
1589 | var cx = context.cx + dx;
1590 | var cy = context.cy + dy;
1591 | var triple = context.triple;
1592 | var connectedNode = null;
1593 |
1594 | if (triple) {
1595 | var sourceNode = triple.sourceNode();
1596 | var targetNode = triple.targetNode();
1597 |
1598 | if (sourceNode && !targetNode)
1599 | connectedNode = sourceNode;
1600 | else if (!sourceNode && targetNode)
1601 | connectedNode = targetNode;
1602 | }
1603 |
1604 | if (connectedNode) {
1605 | // only one node connected
1606 | var x = cx - connectedNode.cx();
1607 | var y = cy - connectedNode.cy();
1608 | triple.updateLinkAngle(Math.atan2(y, x));
1609 | triple.skipNextUpdate(true);
1610 | } else if (!triple || component.content()) {
1611 | // not connected or link has content
1612 | // (except two nodes connected but link has no content)
1613 | component.cx(cx);
1614 | component.cy(cy);
1615 | component.sourceX(context.sourceX + dx);
1616 | component.sourceY(context.sourceY + dy);
1617 | component.targetX(context.targetX + dx);
1618 | component.targetY(context.targetY + dy);
1619 | }
1620 | } else if (component instanceof Connector) {
1621 | var x = context.x + dx;
1622 | var y = context.y + dy;
1623 | var type = context.type;
1624 | var link = context.link;
1625 |
1626 | var triple = helper.firstInstance(link.relations(), Triple);
1627 | var connectedNode = triple ? triple[type + 'Node']() : null;
1628 | var node = this.componentList().fromPoint(Node, x, y);
1629 |
1630 | if (connectedNode && connectedNode === node) {
1631 | // already connected (do nothing)
1632 | return;
1633 | }
1634 |
1635 | var anotherType = Cmap.anotherConnectionType(type);
1636 | var anotherSideNode = triple ? triple[anotherType + 'Node']() : null;
1637 |
1638 | if (connectedNode && connectedNode !== node) {
1639 | this.disconnect(type, connectedNode, link);
1640 | connectedNode = null;
1641 | }
1642 |
1643 | var needsConnect = !connectedNode && node && anotherSideNode !== node;
1644 |
1645 | if (needsConnect) {
1646 | if (anotherSideNode) {
1647 | var p = triple.connectedPoint(node, anotherSideNode.cx(), anotherSideNode.cy());
1648 |
1649 | link[type + 'X'](p.x);
1650 | link[type + 'Y'](p.y);
1651 |
1652 | triple.update(link);
1653 | triple.skipNextUpdate(true);
1654 | }
1655 |
1656 | this.connect(type, node, link);
1657 | } else {
1658 | link[type + 'X'](x);
1659 | link[type + 'Y'](y);
1660 |
1661 | if (!anotherSideNode)
1662 | link.straighten();
1663 | }
1664 | }
1665 | };
1666 |
1667 | Cmap.prototype.onend = function(dx, dy, event) {
1668 | var context = this.dragContext();
1669 |
1670 | var component = context.component;
1671 |
1672 | if (!component)
1673 | return;
1674 |
1675 | if (!context.draggable)
1676 | return;
1677 |
1678 | this.unfixScrollSize();
1679 | };
1680 |
1681 | Cmap.prototype.fixScrollSize = function() {
1682 | var element = this.element();
1683 |
1684 | var clientWidth = dom.clientWidth(element);
1685 | var clientHeight = dom.clientHeight(element);
1686 | var scrollWidth = dom.scrollWidth(element);
1687 | var scrollHeight = dom.scrollHeight(element);
1688 |
1689 | // check if scrolled
1690 | if (clientWidth === scrollWidth && clientHeight === scrollHeight)
1691 | return;
1692 |
1693 | var translate = 'translate(' + (scrollWidth - 1) + 'px, ' + (scrollHeight - 1) + 'px)';
1694 |
1695 | dom.css(this.retainerElement(), {
1696 | msTransform: translate,
1697 | transform: translate,
1698 | webkitTransform: translate
1699 | });
1700 | };
1701 |
1702 | Cmap.prototype.unfixScrollSize = function() {
1703 | var translate = 'translate(-1px, -1px)';
1704 |
1705 | dom.css(this.retainerElement(), {
1706 | msTransform: translate,
1707 | transform: translate,
1708 | webkitTransform: translate
1709 | });
1710 | };
1711 |
1712 | Cmap.prototype.style = function() {
1713 | return {
1714 | color: '#333',
1715 | cursor: 'default',
1716 | fontFamily: 'sans-serif',
1717 | fontSize: '14px',
1718 | height: '100%',
1719 | MozUserSelect: 'none',
1720 | msUserSelect: 'none',
1721 | overflow: 'auto',
1722 | position: 'relative',
1723 | userSelect: 'none',
1724 | webkitUserSelect: 'none',
1725 | width: '100%'
1726 | };
1727 | };
1728 |
1729 | Cmap.prototype.retainerStyle = function() {
1730 | return {
1731 | height: '1px',
1732 | pointerEvents: 'none',
1733 | position: 'absolute',
1734 | width: '1px'
1735 | };
1736 | };
1737 |
1738 | Cmap.prototype.redraw = function() {
1739 | var rootElement = this.rootElement();
1740 |
1741 | if (!rootElement) {
1742 | rootElement = dom.body();
1743 | dom.css(rootElement, {
1744 | height: '100vh',
1745 | margin: '0',
1746 | width: '100vw'
1747 | });
1748 | this.rootElement(rootElement);
1749 | }
1750 |
1751 | var element = dom.el('
');
1752 | dom.draggable(element, this.onstart.bind(this), this.onmove.bind(this), this.onend.bind(this));
1753 | this.element(element);
1754 |
1755 | this.componentList().toArray().forEach(function(component) {
1756 | component.parentElement(element);
1757 | });
1758 |
1759 | var retainerElement = dom.el('
');
1760 | dom.css(retainerElement, this.retainerStyle());
1761 | dom.append(element, retainerElement);
1762 | this.retainerElement(retainerElement);
1763 |
1764 | // set initial position of retainer
1765 | this.unfixScrollSize();
1766 |
1767 | dom.css(element, this.style());
1768 | dom.append(rootElement, element);
1769 | };
1770 |
1771 | Cmap.anotherConnectionType = function(type) {
1772 | if (type === Cmap.CONNECTION_TYPE_SOURCE)
1773 | return Cmap.CONNECTION_TYPE_TARGET;
1774 | else if (type === Cmap.CONNECTION_TYPE_TARGET)
1775 | return Cmap.CONNECTION_TYPE_SOURCE;
1776 | };
1777 |
1778 | Cmap.CONNECTION_TYPE_SOURCE = 'source';
1779 | Cmap.CONNECTION_TYPE_TARGET = 'target';
1780 |
1781 | var ComponentModule = function(component, cmap) {
1782 | this.component = component;
1783 | this.cmap = cmap;
1784 | this.wrapper = helper.wrap(this, cmap.component);
1785 | };
1786 |
1787 | ComponentModule.prototype.attr = function(key, value) {
1788 | var component = this.component;
1789 | var attributeKeys = this.constructor.attributeKeys();
1790 |
1791 | if (typeof key === 'undefined') {
1792 | var props = {};
1793 |
1794 | attributeKeys.forEach(function(key) {
1795 | props[key] = component[key]();
1796 | });
1797 |
1798 | return props;
1799 | }
1800 |
1801 | if (helper.isPlainObject(key)) {
1802 | var props = key;
1803 |
1804 | for (key in props) {
1805 | this.attr(key, props[key]);
1806 | }
1807 |
1808 | return;
1809 | }
1810 |
1811 | if (attributeKeys.indexOf(key) === -1)
1812 | return;
1813 |
1814 | if (typeof value === 'undefined')
1815 | return component[key]();
1816 |
1817 | component[key](value);
1818 | };
1819 |
1820 | ComponentModule.prototype.remove = function() {
1821 | this.cmap.component.remove(this.component);
1822 | helper.deactivate(this.wrapper);
1823 |
1824 | this.component = null;
1825 | this.cmap = null;
1826 | this.wrapper = null;
1827 | };
1828 |
1829 | ComponentModule.prototype.toFront = function() {
1830 | this.cmap.component.toFront(this.component);
1831 | };
1832 |
1833 | ComponentModule.prototype.element = function() {
1834 | return this.component.element();
1835 | };
1836 |
1837 | ComponentModule.prototype.redraw = function() {
1838 | this.component.redraw();
1839 | };
1840 |
1841 | ComponentModule.prototype.draggable = function(enabled) {
1842 | var component = this.component;
1843 | var cmap = this.cmap;
1844 |
1845 | if (typeof enabled === 'undefined')
1846 | return cmap.component.dragEnabled(component);
1847 |
1848 | if (enabled)
1849 | cmap.component.enableDrag(component);
1850 | else
1851 | cmap.component.disableDrag(component);
1852 | };
1853 |
1854 | ComponentModule.attributeKeys = function() {
1855 | return [];
1856 | };
1857 |
1858 | var NodeModule = helper.inherits(function(props, cmap) {
1859 | var component = new Node(helper.pick(props, NodeModule.attributeKeys()));
1860 |
1861 | NodeModule.super_.call(this, component, cmap);
1862 | }, ComponentModule);
1863 |
1864 | NodeModule.prototype.remove = function() {
1865 | this.cmap.nodeModuleList.remove(this);
1866 | NodeModule.super_.prototype.remove.call(this);
1867 | };
1868 |
1869 | NodeModule.attributeKeys = function() {
1870 | return [
1871 | 'content',
1872 | 'contentType',
1873 | 'x',
1874 | 'y',
1875 | 'width',
1876 | 'height',
1877 | 'backgroundColor',
1878 | 'borderColor',
1879 | 'borderWidth',
1880 | 'textColor'
1881 | ];
1882 | };
1883 |
1884 | var LinkModule = helper.inherits(function(props, cmap) {
1885 | var component = new Link(helper.pick(props, LinkModule.attributeKeys()));
1886 |
1887 | LinkModule.super_.call(this, component, cmap);
1888 | }, ComponentModule);
1889 |
1890 | LinkModule.prototype.sourceNode = function(node) {
1891 | return LinkModule.connectNode(this, Cmap.CONNECTION_TYPE_SOURCE, node);
1892 | };
1893 |
1894 | LinkModule.prototype.targetNode = function(node) {
1895 | return LinkModule.connectNode(this, Cmap.CONNECTION_TYPE_TARGET, node);
1896 | };
1897 |
1898 | LinkModule.prototype.sourceConnectorEnabled = function(enabled) {
1899 | return LinkModule.connectorEnabled(this, Cmap.CONNECTION_TYPE_SOURCE, enabled);
1900 | };
1901 |
1902 | LinkModule.prototype.targetConnectorEnabled = function(enabled) {
1903 | return LinkModule.connectorEnabled(this, Cmap.CONNECTION_TYPE_TARGET, enabled);
1904 | };
1905 |
1906 | LinkModule.attributeKeys = function() {
1907 | return [
1908 | 'content',
1909 | 'contentType',
1910 | 'cx',
1911 | 'cy',
1912 | 'width',
1913 | 'height',
1914 | 'backgroundColor',
1915 | 'borderColor',
1916 | 'borderWidth',
1917 | 'textColor',
1918 | 'sourceX',
1919 | 'sourceY',
1920 | 'targetX',
1921 | 'targetY',
1922 | 'lineColor',
1923 | 'lineWidth',
1924 | 'hasArrow'
1925 | ];
1926 | };
1927 |
1928 | LinkModule.connectNode = function(module, type, node) {
1929 | var component = module.component;
1930 | var cmap = module.cmap;
1931 |
1932 | var cmapComponent = cmap.component;
1933 | var connectedNodeComponent = cmapComponent.connectedNode(type, component);
1934 |
1935 | if (typeof node === 'undefined') {
1936 | if (!connectedNodeComponent)
1937 | return null;
1938 |
1939 | var connectedNode = cmap.nodeModuleList.fromComponent(connectedNodeComponent);
1940 | return connectedNode.wrapper;
1941 | }
1942 |
1943 | if (node !== null) {
1944 | if (typeof node !== 'function')
1945 | throw TypeError('Invalid node');
1946 |
1947 | // unwrap node module
1948 | node = node(cmap.component);
1949 |
1950 | if (!(node instanceof NodeModule))
1951 | throw TypeError('Invalid node');
1952 |
1953 | // cannot connect the same node to the source and the target of the link
1954 | var anotherType = Cmap.anotherConnectionType(type);
1955 | var anotherNodeComponent = cmapComponent.connectedNode(anotherType, component);
1956 |
1957 | if (anotherNodeComponent === node.component)
1958 | return;
1959 | }
1960 |
1961 | if (connectedNodeComponent)
1962 | cmapComponent.disconnect(type, connectedNodeComponent, component);
1963 |
1964 | if (node === null)
1965 | return;
1966 |
1967 | cmapComponent.connect(type, node.component, component);
1968 | };
1969 |
1970 | LinkModule.connectorEnabled = function(module, type, enabled) {
1971 | var component = module.component;
1972 | var cmap = module.cmap;
1973 |
1974 | var cmapComponent = cmap.component;
1975 |
1976 | if (typeof enabled === 'undefined')
1977 | return cmapComponent.connectorEnabled(type, component);
1978 |
1979 | if (enabled) {
1980 | cmapComponent.enableConnector(type, component);
1981 |
1982 | // show the connector if another connector is showing
1983 | var anotherType = Cmap.anotherConnectionType(type);
1984 |
1985 | if (cmapComponent.connectorVisible(anotherType, component))
1986 | cmapComponent.showConnector(type, component);
1987 | } else {
1988 | cmapComponent.disableConnector(type, component);
1989 | }
1990 | };
1991 |
1992 | var NodeModuleList = helper.inherits(function() {
1993 | NodeModuleList.super_.call(this);
1994 | }, helper.List);
1995 |
1996 | NodeModuleList.prototype.fromComponent = function(component) {
1997 | var data = this.data;
1998 |
1999 | for (var i = 0, len = data.length; i < len; i++) {
2000 | var node = data[i];
2001 |
2002 | if (node.component === component)
2003 | return node;
2004 | }
2005 |
2006 | return null;
2007 | };
2008 |
2009 | var CmapModule = function(element) {
2010 | if (!(this instanceof CmapModule))
2011 | return new CmapModule(element);
2012 |
2013 | this.component = new Cmap(element);
2014 | this.nodeModuleList = new NodeModuleList();
2015 |
2016 | return helper.wrap(this, this.component);
2017 | };
2018 |
2019 | CmapModule.prototype.node = function(props) {
2020 | if (typeof props !== 'undefined' && !helper.isPlainObject(props))
2021 | throw TypeError('Type error');
2022 |
2023 | var node = new NodeModule(props, this);
2024 |
2025 | this.component.add(node.component);
2026 | this.nodeModuleList.add(node);
2027 |
2028 | return node.wrapper;
2029 | };
2030 |
2031 | CmapModule.prototype.link = function(props) {
2032 | if (typeof props !== 'undefined' && !helper.isPlainObject(props))
2033 | throw TypeError('Type error');
2034 |
2035 | var link = new LinkModule(props, this);
2036 |
2037 | this.component.add(link.component);
2038 |
2039 | return link.wrapper;
2040 | };
2041 |
2042 | if (typeof module !== 'undefined' && module.exports)
2043 | module.exports = CmapModule;
2044 | else
2045 | window.Cmap = CmapModule;
2046 | })();
2047 |
--------------------------------------------------------------------------------