ext-connector.js 15 KB

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