ext-connector.js 16 KB

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