ext-connector.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. /*
  2. * ext-connector.js
  3. *
  4. * Licensed under the Apache License, Version 2
  5. *
  6. * Copyright(c) 2010 Alexis Deveria
  7. *
  8. */
  9. svgEditor.addExtension("Connector", function(S) {
  10. var svgcontent = S.svgcontent,
  11. svgroot = S.svgroot,
  12. getNextId = S.getNextId,
  13. getElem = S.getElem,
  14. addElem = S.addSvgElementFromJson,
  15. selManager = S.selectorManager,
  16. curConfig = svgEditor.curConfig,
  17. started = false,
  18. start_x,
  19. start_y,
  20. cur_line,
  21. start_elem,
  22. end_elem,
  23. connections = [],
  24. conn_sel = ".se_connector",
  25. se_ns,
  26. // connect_str = "-SE_CONNECT-",
  27. selElems = [];
  28. elData = $.data;
  29. var lang_list = {
  30. "en":[
  31. {"id": "mode_connect", "title": "Connect two objects" }
  32. ],
  33. "fr":[
  34. {"id": "mode_connect", "title": "Connecter deux objets"}
  35. ]
  36. };
  37. function getOffset(side, line) {
  38. var give_offset = !!line.getAttribute('marker-' + side);
  39. // var give_offset = $(line).data(side+'_off');
  40. // TODO: Make this number (5) be based on marker width/height
  41. var size = line.getAttribute('stroke-width') * 5;
  42. return give_offset ? size : 0;
  43. }
  44. function showPanel(on) {
  45. var conn_rules = $('#connector_rules');
  46. if(!conn_rules.length) {
  47. conn_rules = $('<style id="connector_rules"><\/style>').appendTo('head');
  48. }
  49. conn_rules.text(!on?"":"#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }");
  50. $('#connector_panel').toggle(on);
  51. }
  52. function setPoint(elem, pos, x, y, setMid) {
  53. var pts = elem.points;
  54. var pt = svgroot.createSVGPoint();
  55. pt.x = x;
  56. pt.y = y;
  57. if(pos === 'end') pos = pts.numberOfItems-1;
  58. // TODO: Test for this on init, then use alt only if needed
  59. try {
  60. pts.replaceItem(pt, pos);
  61. } catch(err) {
  62. // Should only occur in FF which formats points attr as "n,n n,n", so just split
  63. var pt_arr = elem.getAttribute("points").split(" ");
  64. for(var i=0; i< pt_arr.length; i++) {
  65. if(i == pos) {
  66. pt_arr[i] = x + ',' + y;
  67. }
  68. }
  69. elem.setAttribute("points",pt_arr.join(" "));
  70. }
  71. if(setMid) {
  72. // Add center point
  73. var pt_start = pts.getItem(0);
  74. var pt_end = pts.getItem(pts.numberOfItems-1);
  75. setPoint(elem, 1, (pt_end.x + pt_start.x)/2, (pt_end.y + pt_start.y)/2);
  76. }
  77. }
  78. function updateLine(diff_x, diff_y) {
  79. // Update line with element
  80. var i = connections.length;
  81. while(i--) {
  82. var conn = connections[i];
  83. var line = conn.connector;
  84. var elem = conn.elem;
  85. var pre = conn.is_start?'start':'end';
  86. // var sw = line.getAttribute('stroke-width') * 5;
  87. // Update bbox for this element
  88. var bb = elData(line, pre+'_bb');
  89. bb.x = conn.start_x + diff_x;
  90. bb.y = conn.start_y + diff_y;
  91. elData(line, pre+'_bb', bb);
  92. var alt_pre = conn.is_start?'end':'start';
  93. // Get center pt of connected element
  94. var bb2 = elData(line, alt_pre+'_bb');
  95. var src_x = bb2.x + bb2.width/2;
  96. var src_y = bb2.y + bb2.height/2;
  97. // Set point of element being moved
  98. var pt = getBBintersect(src_x, src_y, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0
  99. setPoint(line, conn.is_start?0:'end', pt.x, pt.y, true);
  100. // Set point of connected element
  101. var pt2 = getBBintersect(pt.x, pt.y, elData(line, alt_pre + '_bb'), getOffset(alt_pre, line));
  102. setPoint(line, conn.is_start?'end':0, pt2.x, pt2.y, true);
  103. }
  104. }
  105. function findConnectors(elems) {
  106. if(!elems) elems = selElems;
  107. var connectors = $(svgcontent).find(conn_sel);
  108. connections = [];
  109. // Loop through connectors to see if one is connected to the element
  110. connectors.each(function() {
  111. var start = elData(this, "c_start");
  112. var end = elData(this, "c_end");
  113. var parts = [getElem(start), getElem(end)];
  114. for(var i=0; i<2; i++) {
  115. var c_elem = parts[i];
  116. var add_this = false;
  117. // The connected element might be part of a selected group
  118. $(c_elem).parents().each(function() {
  119. if($.inArray(this, elems) !== -1) {
  120. // Pretend this element is selected
  121. add_this = true;
  122. }
  123. });
  124. if(!c_elem || !c_elem.parentNode) {
  125. $(this).remove();
  126. continue;
  127. }
  128. if($.inArray(c_elem, elems) !== -1 || add_this) {
  129. var bb = svgCanvas.getStrokedBBox([c_elem]);
  130. connections.push({
  131. elem: c_elem,
  132. connector: this,
  133. is_start: (i === 0),
  134. start_x: bb.x,
  135. start_y: bb.y
  136. });
  137. }
  138. }
  139. });
  140. }
  141. function updateConnectors(elems) {
  142. // Updates connector lines based on selected elements
  143. // Is not used on mousemove, as it runs getStrokedBBox every time,
  144. // which isn't necessary there.
  145. findConnectors(elems);
  146. if(connections.length) {
  147. // Update line with element
  148. var i = connections.length;
  149. while(i--) {
  150. var conn = connections[i];
  151. var line = conn.connector;
  152. var elem = conn.elem;
  153. var sw = line.getAttribute('stroke-width') * 5;
  154. var pre = conn.is_start?'start':'end';
  155. // Update bbox for this element
  156. var bb = svgCanvas.getStrokedBBox([elem]);
  157. bb.x = conn.start_x;
  158. bb.y = conn.start_y;
  159. elData(line, pre+'_bb', bb);
  160. var add_offset = elData(line, pre+'_off');
  161. var alt_pre = conn.is_start?'end':'start';
  162. // Get center pt of connected element
  163. var bb2 = elData(line, alt_pre+'_bb');
  164. var src_x = bb2.x + bb2.width/2;
  165. var src_y = bb2.y + bb2.height/2;
  166. // Set point of element being moved
  167. var pt = getBBintersect(src_x, src_y, bb, getOffset(pre, line));
  168. setPoint(line, conn.is_start?0:'end', pt.x, pt.y, true);
  169. // Set point of connected element
  170. var pt2 = getBBintersect(pt.x, pt.y, elData(line, alt_pre + '_bb'), getOffset(alt_pre, line));
  171. setPoint(line, conn.is_start?'end':0, pt2.x, pt2.y, true);
  172. // Update points attribute manually for webkit
  173. if(navigator.userAgent.indexOf('AppleWebKit') != -1) {
  174. var pts = line.points;
  175. var len = pts.numberOfItems;
  176. var pt_arr = Array(len);
  177. for(var j=0; j< len; j++) {
  178. var pt = pts.getItem(j);
  179. pt_arr[j] = pt.x + ',' + pt.y;
  180. }
  181. line.setAttribute("points",pt_arr.join(" "));
  182. }
  183. }
  184. }
  185. }
  186. function getBBintersect(x, y, bb, offset) {
  187. if(offset) {
  188. offset -= 0;
  189. bb = $.extend({}, bb);
  190. bb.width += offset;
  191. bb.height += offset;
  192. bb.x -= offset/2;
  193. bb.y -= offset/2;
  194. }
  195. var mid_x = bb.x + bb.width/2;
  196. var mid_y = bb.y + bb.height/2;
  197. var len_x = x - mid_x;
  198. var len_y = y - mid_y;
  199. var slope = Math.abs(len_y/len_x);
  200. var ratio;
  201. if(slope < bb.height/bb.width) {
  202. ratio = (bb.width/2) / Math.abs(len_x);
  203. } else {
  204. ratio = (bb.height/2) / Math.abs(len_y);
  205. }
  206. return {
  207. x: mid_x + len_x * ratio,
  208. y: mid_y + len_y * ratio
  209. }
  210. }
  211. // Do once
  212. (function() {
  213. var gse = svgCanvas.groupSelectedElements;
  214. svgCanvas.groupSelectedElements = function() {
  215. svgCanvas.removeFromSelection($(conn_sel).toArray());
  216. return gse.apply(this, arguments);
  217. }
  218. var mse = svgCanvas.moveSelectedElements;
  219. svgCanvas.moveSelectedElements = function() {
  220. svgCanvas.removeFromSelection($(conn_sel).toArray());
  221. var cmd = mse.apply(this, arguments);
  222. updateConnectors();
  223. return cmd;
  224. }
  225. se_ns = svgCanvas.getEditorNS();
  226. }());
  227. // Do on reset
  228. function init() {
  229. // Make sure all connectors have data set
  230. $(svgcontent).find('*').each(function() {
  231. var conn = this.getAttributeNS(se_ns, "connector");
  232. if(conn) {
  233. this.setAttribute('class', conn_sel.substr(1));
  234. var conn_data = conn.split(' ');
  235. var sbb = svgCanvas.getStrokedBBox([getElem(conn_data[0])]);
  236. var ebb = svgCanvas.getStrokedBBox([getElem(conn_data[1])]);
  237. $(this).data('c_start',conn_data[0])
  238. .data('c_end',conn_data[1])
  239. .data('start_bb', sbb)
  240. .data('end_bb', ebb);
  241. svgCanvas.getEditorNS(true);
  242. }
  243. });
  244. // updateConnectors();
  245. }
  246. // $(svgroot).parent().mousemove(function(e) {
  247. // // if(started
  248. // // || svgCanvas.getMode() != "connector"
  249. // // || e.target.parentNode.parentNode != svgcontent) return;
  250. //
  251. // console.log('y')
  252. // // if(e.target.parentNode.parentNode === svgcontent) {
  253. // //
  254. // // }
  255. // });
  256. return {
  257. name: "Connector",
  258. svgicons: "images/conn.svg",
  259. buttons: [{
  260. id: "mode_connect",
  261. type: "mode",
  262. icon: "images/cut.png",
  263. title: "Connect two objects",
  264. includeWith: {
  265. button: '#tool_line',
  266. isDefault: false,
  267. position: 1
  268. },
  269. events: {
  270. 'click': function() {
  271. svgCanvas.setMode("connector");
  272. }
  273. }
  274. }],
  275. addLangData: function(lang) {
  276. return {
  277. data: lang_list[lang]
  278. };
  279. },
  280. mouseDown: function(opts) {
  281. var e = opts.event;
  282. start_x = opts.start_x,
  283. start_y = opts.start_y;
  284. var mode = svgCanvas.getMode();
  285. if(mode == "connector") {
  286. if(started) return;
  287. var mouse_target = e.target;
  288. var parents = $(mouse_target).parents();
  289. if($.inArray(svgcontent, parents) != -1) {
  290. // Connectable element
  291. // If child of foreignObject, use parent
  292. var fo = $(mouse_target).closest("foreignObject");
  293. start_elem = fo.length ? fo[0] : mouse_target;
  294. // Get center of source element
  295. var bb = svgCanvas.getStrokedBBox([start_elem]);
  296. var x = bb.x + bb.width/2;
  297. var y = bb.y + bb.height/2;
  298. started = true;
  299. cur_line = addElem({
  300. "element": "polyline",
  301. "attr": {
  302. "id": getNextId(),
  303. "points": (x+','+y+' '+x+','+y+' '+start_x+','+start_y),
  304. "stroke": '#' + curConfig.initStroke.color,
  305. "stroke-width": (!start_elem.stroke_width || start_elem.stroke_width == 0) ? curConfig.initStroke.width : start_elem.stroke_width,
  306. "fill": "none",
  307. "opacity": curConfig.initStroke.opacity,
  308. "style": "pointer-events:none"
  309. }
  310. });
  311. elData(cur_line, 'start_bb', bb);
  312. }
  313. return {
  314. started: true
  315. };
  316. } else if(mode == "select") {
  317. findConnectors();
  318. }
  319. },
  320. mouseMove: function(opts) {
  321. var zoom = svgCanvas.getZoom();
  322. var e = opts.event;
  323. var x = opts.mouse_x/zoom;
  324. var y = opts.mouse_y/zoom;
  325. var diff_x = x - start_x,
  326. diff_y = y - start_y;
  327. var mode = svgCanvas.getMode();
  328. if(mode == "connector" && started) {
  329. var sw = cur_line.getAttribute('stroke-width') * 3;
  330. // Set start point (adjusts based on bb)
  331. var pt = getBBintersect(x, y, elData(cur_line, 'start_bb'), getOffset('start', cur_line));
  332. start_x = pt.x;
  333. start_y = pt.y;
  334. setPoint(cur_line, 0, pt.x, pt.y, true);
  335. // Set end point
  336. setPoint(cur_line, 'end', x, y, true);
  337. } else if(mode == "select") {
  338. var slen = selElems.length;
  339. while(slen--) {
  340. var elem = selElems[slen];
  341. // Look for selected connector elements
  342. if(elem && elData(elem, 'c_start')) {
  343. // Remove the "translate" transform given to move
  344. svgCanvas.removeFromSelection([elem]);
  345. svgCanvas.getTransformList(elem).clear();
  346. }
  347. }
  348. if(connections.length) {
  349. updateLine(diff_x, diff_y);
  350. }
  351. }
  352. },
  353. mouseUp: function(opts) {
  354. var zoom = svgCanvas.getZoom();
  355. var e = opts.event,
  356. x = opts.mouse_x/zoom,
  357. y = opts.mouse_y/zoom,
  358. mouse_target = e.target;
  359. if(svgCanvas.getMode() == "connector") {
  360. var fo = $(mouse_target).closest("foreignObject");
  361. if(fo.length) mouse_target = fo[0];
  362. var parents = $(mouse_target).parents();
  363. if(mouse_target == start_elem) {
  364. // Start line through click
  365. started = true;
  366. return {
  367. keep: true,
  368. element: null,
  369. started: started
  370. }
  371. } else if($.inArray(svgcontent, parents) === -1) {
  372. // Not a valid target element, so remove line
  373. $(cur_line).remove();
  374. started = false;
  375. return {
  376. keep: false,
  377. element: null,
  378. started: started
  379. }
  380. } else {
  381. // Valid end element
  382. end_elem = mouse_target;
  383. var start_id = start_elem.id, end_id = end_elem.id;
  384. var conn_str = start_id + " " + end_id;
  385. var alt_str = end_id + " " + start_id;
  386. // Don't create connector if one already exists
  387. var dupe = $(svgcontent).find(conn_sel).filter(function() {
  388. var conn = this.getAttributeNS(se_ns, "connector");
  389. if(conn == conn_str || conn == alt_str) return true;
  390. });
  391. if(dupe.length) {
  392. $(cur_line).remove();
  393. return {
  394. keep: false,
  395. element: null,
  396. started: false
  397. }
  398. }
  399. var bb = svgCanvas.getStrokedBBox([end_elem]);
  400. var pt = getBBintersect(start_x, start_y, bb, getOffset('start', cur_line));
  401. setPoint(cur_line, 'end', pt.x, pt.y, true);
  402. $(cur_line)
  403. .data("c_start", start_id)
  404. .data("c_end", end_id)
  405. .data("end_bb", bb);
  406. se_ns = svgCanvas.getEditorNS(true);
  407. cur_line.setAttributeNS(se_ns, "se:connector", conn_str);
  408. cur_line.setAttribute('class', conn_sel.substr(1));
  409. cur_line.setAttribute('opacity', 1);
  410. svgCanvas.addToSelection([cur_line]);
  411. svgCanvas.moveToBottomSelectedElement();
  412. selManager.requestSelector(cur_line).showGrips(false);
  413. started = false;
  414. return {
  415. keep: true,
  416. element: cur_line,
  417. started: started
  418. }
  419. }
  420. }
  421. },
  422. selectedChanged: function(opts) {
  423. // TODO: Find better way to skip operations if no connectors are in use
  424. if(!$(svgcontent).find(conn_sel).length) return;
  425. if(svgCanvas.getMode() == 'connector') {
  426. svgCanvas.setMode('select');
  427. }
  428. // Use this to update the current selected elements
  429. selElems = opts.elems;
  430. var i = selElems.length;
  431. while(i--) {
  432. var elem = selElems[i];
  433. if(elem && elData(elem, 'c_start')) {
  434. selManager.requestSelector(elem).showGrips(false);
  435. if(opts.selectedElement && !opts.multiselected) {
  436. // TODO: Set up context tools and hide most regular line tools
  437. showPanel(true);
  438. } else {
  439. showPanel(false);
  440. }
  441. } else {
  442. showPanel(false);
  443. }
  444. }
  445. updateConnectors();
  446. },
  447. elementChanged: function(opts) {
  448. var elem = opts.elems[0];
  449. if (elem && elem.tagName == 'svg' && elem.id == "svgcontent") {
  450. // Update svgcontent (can change on import)
  451. svgcontent = elem;
  452. init();
  453. }
  454. // Has marker, so change offset
  455. if(elem && (
  456. elem.getAttribute("marker-start") ||
  457. elem.getAttribute("marker-mid") ||
  458. elem.getAttribute("marker-end")
  459. )) {
  460. var start = elem.getAttribute("marker-start");
  461. var mid = elem.getAttribute("marker-mid");
  462. var end = elem.getAttribute("marker-end");
  463. cur_line = elem;
  464. $(elem)
  465. .data("start_off", !!start)
  466. .data("end_off", !!end);
  467. if(elem.tagName == "line" && mid) {
  468. // Convert to polyline to accept mid-arrow
  469. var x1 = elem.getAttribute('x1')-0;
  470. var x2 = elem.getAttribute('x2')-0;
  471. var y1 = elem.getAttribute('y1')-0;
  472. var y2 = elem.getAttribute('y2')-0;
  473. var id = elem.id;
  474. var mid_pt = (' '+((x1+x2)/2)+','+((y1+y2)/2) + ' ');
  475. var pline = addElem({
  476. "element": "polyline",
  477. "attr": {
  478. "points": (x1+','+y1+ mid_pt +x2+','+y2),
  479. "stroke": elem.getAttribute('stroke'),
  480. "stroke-width": elem.getAttribute('stroke-width'),
  481. "marker-mid": mid,
  482. "fill": "none",
  483. "opacity": elem.getAttribute('opacity') || 1
  484. }
  485. });
  486. $(elem).after(pline).remove();
  487. svgCanvas.clearSelection();
  488. pline.id = id;
  489. svgCanvas.addToSelection([pline]);
  490. elem = pline;
  491. }
  492. }
  493. // Update line if it's a connector
  494. if(elem.getAttribute('class') == conn_sel.substr(1)) {
  495. var start = getElem(elData(elem, 'c_start'));
  496. updateConnectors([start]);
  497. } else {
  498. updateConnectors();
  499. }
  500. },
  501. toolButtonStateUpdate: function(opts) {
  502. if(opts.nostroke) {
  503. if ($('#mode_connect').hasClass('tool_button_current')) {
  504. clickSelect();
  505. }
  506. }
  507. $('#mode_connect')
  508. .toggleClass('disabled',opts.nostroke);
  509. }
  510. };
  511. });