ModelDiagramView.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /**
  2. * Licensed Materials - Property of IBM IBM Cognos Products: Modeling UI (C) Copyright IBM Corp. 2016, 2020
  3. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP
  4. * Schedule Contract with IBM Corp.
  5. */
  6. define([
  7. 'jquery',
  8. 'underscore',
  9. 'bi/glass/app/util/View',
  10. './DiagramNode',
  11. './DiagramLink',
  12. './util/StringUtils',
  13. './util/Queue',
  14. '../InternalBridge'
  15. ], function ($, _, View, DiagramNode, DiagramLink, StringUtils, Queue, InternalBridge) {
  16. var d3;
  17. var MAX_ZOOM_SCALE = 6;
  18. var MIN_ZOOM_SCALE = 0.2
  19. // Ensures that nodes in diagram move away from each other
  20. var D3_FORCE_CHARGE = -1000;
  21. // ... but not too much
  22. var D3_FORCE_CHARGE_DISTANCE = 450;
  23. // Ensures connected nodes stay together at this distance
  24. var D3_FORCE_LINK_DISTANCE = 300;
  25. // Ensures that links do not drag nodes away from the dropped spot
  26. var D3_FORCE_LINK_STRENGTH = 0;
  27. var ModelDiagramView = View.extend({
  28. _d3ForceContext: null,
  29. _SVGContext: null,
  30. /*--D3 DATA--*/
  31. // List of current Nodes
  32. _nodeList: [],
  33. // List of current Links
  34. _linkList: [],
  35. _selectedNodeList: [],
  36. /*--INTERNAL DATA--*/
  37. // Saves positional information of nodes for Drag-n-Drop undo/redo and so on
  38. _nodePosition: {},
  39. // Which nodes (by identifier) are connected to which
  40. // nodes (as array of identifiers)
  41. _adjacencyList: {},
  42. _diagramContentProvider: null,
  43. initialNodes: null,
  44. initialLinks: null,
  45. /**
  46. * Initialize the diagram view. Requires "options._diagramContentProvider" to be defined
  47. *
  48. * @param {object}
  49. * options - custom options to add to the diagram view
  50. * @param {object}
  51. * options._diagramContentProvider - [REQUIRED] Provides implementation of a diagram content provider API
  52. * @param {array}
  53. * options.initialNodes - Initial nodes wanted in the diagram on first load
  54. * @param {array}
  55. * options.initialLinks - Initial links wanted in the diagram on first load
  56. */
  57. init: function (options) {
  58. ModelDiagramView.inherited('init', this, arguments);
  59. _.extend(this, options);
  60. d3 = options.d3;
  61. this._initializeD3();
  62. },
  63. _handleWindowKeyDown: function (e) {
  64. // To handle escape key during rename.
  65. if (e.which === 27) {
  66. this._onInputBlur();
  67. }
  68. },
  69. remove: function () {
  70. this._d3ForceContext && this._d3ForceContext.stop();
  71. window.removeEventListener('keydown', this._handleWindowKeyDown);
  72. ModelDiagramView.inherited('remove', this);
  73. this._SVGContext.empty();
  74. this.$el.empty();
  75. },
  76. _initializeD3: function () {
  77. var zoomAndPan = this.diagramStore.zoomAndPan;
  78. this._SVGContext = d3.select(this.$el[0]).append('svg').attr('width', '100%').attr('height', '100%');
  79. this._SVGContext.on('click', function() {
  80. if (d3.event.defaultPrevented) return; // dragged
  81. this.diagramStore.clickContainer();
  82. }.bind(this));
  83. this._SVGContext.call(d3.zoom().transform, d3.zoomIdentity.translate(zoomAndPan.translate[0], zoomAndPan.translate[1]).scale(zoomAndPan.scale));
  84. this._SVGContext.call(d3.zoom().scaleExtent([MIN_ZOOM_SCALE, MAX_ZOOM_SCALE]).filter(this._zoomFilter.bind(this)).on('zoom', this._onPan.bind(this))).on('dblclick.zoom', null);
  85. this._SVGContext.diagramContainer = this._SVGContext.append('svg:g').attr('class', 'mui-diagram-container');
  86. this._SVGContext.diagramContainer.append('svg:g').attr('class', 'mui-link-container');
  87. this._SVGContext.diagramContainer.append('svg:g').attr('class', 'mui-node-container');
  88. this._SVGContext.diagramContainer.append('svg:g').attr('class', 'mui-link-upper-container');
  89. this._SVGContext.diagramContainer.append('svg:g').attr('class', 'mui-node-upper-container');
  90. this._handleWindowKeyDown = this._handleWindowKeyDown.bind(this);
  91. window.addEventListener('keydown', this._handleWindowKeyDown);
  92. },
  93. _zoomFilter: function() {
  94. // Ignore right-click events, since it should open context menu, else check wheel events
  95. return !d3.event.button && (d3.event.type !== 'wheel' || !d3.event.ctrlKey);
  96. },
  97. _onPan: function() {
  98. var transform = d3.event.transform;
  99. var scale = d3.event.sourceEvent && d3.event.sourceEvent.type === 'wheel' ? transform.k : this.diagramStore.zoomAndPan.scale;
  100. this.diagramStore.setZoomAndPan({
  101. scale: scale,
  102. translate: [transform.x, transform.y]
  103. });
  104. // remove the rename input if it exists and save changes
  105. this._onInputBlur();
  106. this.diagramStore.hideContextMenu();
  107. },
  108. /**
  109. * Displays the cardinality of a link between nodes
  110. */
  111. showCardinality: function (isVisible) {
  112. this.$el.removeClass('mui-diagram-cardinality-displayed');
  113. if (isVisible) {
  114. this.$el.addClass('mui-diagram-cardinality-displayed');
  115. }
  116. },
  117. isAnimating: function () {
  118. return this._isAnimating;
  119. },
  120. /**
  121. * Will hide any nodes in the diagram that are more than the number of stops away from the selected node.
  122. */
  123. showDegreesOfSeparation: function () {
  124. var allNodeIDs = _.pluck(this._nodeList, 'identifier');
  125. var selectedNodes = this.$el.find('.mui-diagram-selected');
  126. if (this.diagramStore.isContextMode) {
  127. // This is to cope with proposal diagram view
  128. if (this.diagramStore.allVisibleNodes) {
  129. this.allVisibleNodes = this.diagramStore.allVisibleNodes;
  130. }
  131. this._fadeTo(this.allVisibleNodes);
  132. this._fadeTo(_.difference(allNodeIDs, this.allVisibleNodes), true);
  133. } else if (this.diagramStore.areAllNodesVisible) {
  134. this.allVisibleNodes = allNodeIDs;
  135. this._fadeTo(allNodeIDs);
  136. // When no node selected, all nodes are set to faded.
  137. } else if (selectedNodes.length === 0) {
  138. this._fadeTo(allNodeIDs, true);
  139. return;
  140. // FOR EACH 'exploreNode'
  141. } else {
  142. this.allVisibleNodes = [];
  143. _.each(selectedNodes, function (exploreNode) {
  144. exploreNode = ($(exploreNode).attr('class') || '').split(' ');
  145. exploreNode = _.without(exploreNode, 'mui-diagram-node', 'mui-reference-node', 'mui-diagram-selected');
  146. exploreNode = exploreNode.toString();
  147. var visibleNodes = (!this._adjacencyList[exploreNode]) ? [exploreNode] : _.uniq(this._getAdjacentNodes(this.diagramStore.degreesOfSeparation, exploreNode));
  148. this.allVisibleNodes = _.union(this.allVisibleNodes, visibleNodes);
  149. }.bind(this));
  150. this._fadeTo(this.allVisibleNodes);
  151. this._fadeTo(_.difference(allNodeIDs, this.allVisibleNodes), true);
  152. }
  153. // Bring the selected node into the front, so that it won't be covered by other visible nodes.
  154. this.bringNodesToFront(this.$el.find('.mui-diagram-selected'));
  155. },
  156. _fadeTo: function (nodeIds, toFade) {
  157. var nodesToBack = [];
  158. var nodesToFront = [];
  159. var linksToFront = [];
  160. var linksToBack = [];
  161. if (nodeIds && nodeIds.length > 0) {
  162. for (var i=0; i<nodeIds.length; i++) {
  163. var nodeId = nodeIds[i];
  164. var $el = this.$el.find('.mui-diagram-node.' + nodeId);
  165. if (toFade) {
  166. if (this.diagramStore.isContextMode) {
  167. $el.find('.mui-diagram-node-content').hide();
  168. $el.find('.mui-diagram-node-box').hide();
  169. $el.find('.mui-diagram-table-details').hide();
  170. } else {
  171. $el.find('.mui-diagram-node-content').show().addClass('faded');
  172. $el.find('.mui-diagram-node-box').show().addClass('faded');
  173. $el.find('.mui-diagram-table-details').show().addClass('faded');
  174. }
  175. nodesToBack.push($el);
  176. } else {
  177. $el.find('.mui-diagram-node-content').show().removeClass('faded');
  178. $el.find('.mui-diagram-node-box').show().removeClass('faded');
  179. $el.find('.mui-diagram-table-details').show().removeClass('faded');
  180. nodesToFront.push($el);
  181. }
  182. var links = this.$el.find('.mui-diagram-link.' + nodeId);
  183. for (var idx=0; idx<links.length; idx++) {
  184. var link = links[idx];
  185. var visibleLink = _.intersection($(link).attr('class').split(' '), this.allVisibleNodes).length === 2;
  186. var isFaded = $(link).hasClass('faded');
  187. if (!toFade && visibleLink) {
  188. $(link).show();
  189. isFaded && $(link).removeClass('faded');
  190. linksToFront.push($(link));
  191. } else if (toFade) {
  192. if (this.diagramStore.isContextMode) {
  193. linksToBack.push($(link).hide());
  194. } else {
  195. linksToBack.push($(link).show().addClass('faded'));
  196. }
  197. }
  198. }
  199. }
  200. }
  201. this.bringLinksToBack(linksToBack);
  202. this.bringNodesToBack(nodesToBack);
  203. this.bringLinksToFront(linksToFront);
  204. this.bringNodesToFront(nodesToFront);
  205. },
  206. /**
  207. * Bring the selected nodes to the front of the view
  208. *
  209. * @param nodes
  210. *
  211. */
  212. bringNodesToFront: function (nodes) {
  213. this.$el.find('.mui-node-upper-container').append(nodes);
  214. },
  215. bringLinksToFront: function (links) {
  216. this.$el.find('.mui-link-upper-container').append(links);
  217. },
  218. bringNodesToBack: function (nodes) {
  219. this.$el.find('.mui-node-container').append(nodes);
  220. },
  221. bringLinksToBack: function (links) {
  222. this.$el.find('.mui-link-container').append(links);
  223. },
  224. /**
  225. * Will return a list of nodes that are within the number of stops to a selected node
  226. *
  227. * @param {string}
  228. * stops - The number of stops (levels) to explore
  229. * @param {string}
  230. * selectedNode - The root node the function is exploring
  231. */
  232. _getAdjacentNodes: function (stops, selectedNode) {
  233. var q = new Queue();
  234. var visitedNodes = [];
  235. var node;
  236. var depth = stops;
  237. // variables to keep track of when to decrease depth
  238. var countToDepthIncrease = 1,
  239. nextCountToDepthIncrease = 0;
  240. q.enqueue(selectedNode);
  241. while(!q.isEmpty() && depth >= 0) {
  242. node = q.dequeue();
  243. // don't need to re-process nodes
  244. if(!_.contains(visitedNodes, node)) {
  245. // mark node as visited
  246. visitedNodes = _.union(visitedNodes, node);
  247. // add adjacent nodes to queue to be processed
  248. var adjacencyList = this._adjacencyList[node] || [];
  249. for(var i = 0; i < adjacencyList.length; i++) {
  250. q.enqueue(adjacencyList[i]);
  251. nextCountToDepthIncrease++;
  252. }
  253. }
  254. // if we've processed everything from the previous depth,
  255. // move to next level of the graph
  256. if(--countToDepthIncrease === 0) {
  257. countToDepthIncrease = nextCountToDepthIncrease;
  258. nextCountToDepthIncrease = 0;
  259. depth--;
  260. }
  261. }
  262. return visitedNodes;
  263. },
  264. /**
  265. * Add a new node to the diagram
  266. *
  267. * @param {object}
  268. * moserObject - A node object that should be added to the diagram
  269. */
  270. addNode: function (moserObject, options, droppedPosition) {
  271. if (this.diagramStore.droppedPosition && !this.diagramStore.isDnD) {
  272. var id = moserObject.getIdentifier();
  273. if (moserObject !== this.diagramStore.contextMenuFromMoserObject
  274. && this.allVisibleNodes.indexOf(id) > -1) {
  275. this.diagramStore.setPosition(moserObject);
  276. }
  277. }
  278. if (!this._hasPosition(moserObject)) {
  279. this._withNewNodes = true;
  280. }
  281. _.extend(options, {
  282. contentProvider: this._diagramContentProvider,
  283. moserObject: moserObject,
  284. fixed: this._hasPosition(moserObject),
  285. position: this._getPosition(moserObject, droppedPosition),
  286. moserModule: this.moserModule,
  287. isDetailsVisible: this.diagramStore.isDetailsVisible,
  288. tableRowCount: this.diagramStore.getObjectRowCount(moserObject),
  289. statisticsLoading: this.diagramStore.getObjectStatisticsLoading(moserObject)
  290. });
  291. var newNode = new DiagramNode(options);
  292. this._nodeList.push(newNode);
  293. },
  294. /**
  295. * Add a new link to the diagram
  296. *
  297. * @param {object}
  298. * linkObj - A link object that should be added to the diagram
  299. */
  300. addLink: function (linkObj) {
  301. if (!linkObj.left || !linkObj.right) {
  302. return;
  303. }
  304. var leftID = linkObj.left.ref,
  305. rightID = linkObj.right.ref,
  306. useSpec = this.moserModule.getUseSpec(),
  307. leftRefObject = linkObj.left.getReferencedObject(),
  308. rightRefObject = linkObj.right.getReferencedObject(),
  309. leftIsFromPackage = InternalBridge.moserUtils.isPartOfPackage(leftRefObject),
  310. rightIsFromPackage = InternalBridge.moserUtils.isPartOfPackage(rightRefObject);
  311. if( leftIsFromPackage ){
  312. leftID = InternalBridge.moserUtils.getUseSpecByRef(leftRefObject, linkObj.left.ref).identifier;
  313. }
  314. if( rightIsFromPackage ){
  315. rightID = InternalBridge.moserUtils.getUseSpecByRef(rightRefObject, linkObj.right.ref).identifier;
  316. }
  317. var linkSource = _.find(this._nodeList, function (ele) {
  318. return leftID === ele.identifier;
  319. });
  320. var linkTarget = _.find(this._nodeList, function (ele) {
  321. return rightID === ele.identifier;
  322. });
  323. if (!linkSource || !linkTarget) {
  324. return;
  325. }
  326. var currentDiagramLink = _.find(this._linkList, function (ele){
  327. return linkObj.identifier === ele.id;
  328. });
  329. if (currentDiagramLink) {
  330. currentDiagramLink.addARepresentedLink({
  331. linkObj: linkObj,
  332. left: linkObj.left,
  333. link: linkObj.link,
  334. right: linkObj.right
  335. });
  336. } else {
  337. var newLink = new DiagramLink({
  338. source: linkSource,
  339. target: linkTarget,
  340. instanceType: linkObj.instanceType ? linkObj.instanceType.value() : 'copy',
  341. id: linkObj.identifier,
  342. moserModule: this.moserModule,
  343. moserObject: linkObj,
  344. severityInfo: InternalBridge.validationUtils.getHighestSeverityErrorInfo(linkObj)
  345. });
  346. newLink.addARepresentedLink({
  347. linkObj: linkObj,
  348. left: linkObj.left,
  349. link: linkObj.link,
  350. right: linkObj.right
  351. });
  352. linkSource.addAdjacentLink(newLink.id);
  353. linkTarget.addAdjacentLink(newLink.id);
  354. this._linkList.push(newLink);
  355. }
  356. this._adjacencyList[leftID] = this._adjacencyList[leftID] || [];
  357. this._adjacencyList[rightID] = this._adjacencyList[rightID] || [];
  358. // prevent bloat of the adjacencyList - don't need duplicates
  359. if (!_.contains(this._adjacencyList[leftID], rightID)) {
  360. this._adjacencyList[leftID].push(rightID);
  361. }
  362. if (!_.contains(this._adjacencyList[rightID], leftID)) {
  363. this._adjacencyList[rightID].push(leftID);
  364. }
  365. },
  366. /**
  367. * Programmatically selects a node in the diagram (when selecting a node from somewhere OTHER than the diagram)
  368. *
  369. * @param {array}
  370. * identifiers - An array of strings of node IDs to be selected
  371. */
  372. selectNodes: function () {
  373. var selectedNodeIdentifiers = _.map(this.diagramStore.selection, function (node) { return node.getIdentifier(); });
  374. this.$el.find('.mui-diagram-selected').removeClass('mui-diagram-selected');
  375. selectedNodeIdentifiers.forEach(function (id) {
  376. this.$el.find('.mui-diagram-node.' + id).addClass('mui-diagram-selected');
  377. }.bind(this));
  378. this._selectedNodeList = this._nodeList.filter(function(ele) { return selectedNodeIdentifiers.indexOf(ele.identifier) !== -1 });
  379. },
  380. _update: function () {
  381. this._d3ForceContext = d3.forceSimulation(this._nodeList)
  382. .force('charge', d3.forceManyBody().strength(D3_FORCE_CHARGE).distanceMax(D3_FORCE_CHARGE_DISTANCE))
  383. .force('link', d3.forceLink(this._linkList)
  384. .strength(D3_FORCE_LINK_STRENGTH)
  385. .distance(D3_FORCE_LINK_DISTANCE))
  386. .alpha(this._withNewNodes ? 0.1 : 0)
  387. .restart();
  388. var links = this._diagramContentProvider.drawLinks(this._SVGContext, this._linkList);
  389. var nodes = this._diagramContentProvider.drawNodes(this._SVGContext, this._nodeList);
  390. var self = this;
  391. nodes.call(d3.drag().on('start', function(e) {
  392. this.diagramStore.hideContextMenu();
  393. d3.event.sourceEvent.stopPropagation();
  394. }.bind(this)).on('drag', function (d) {
  395. if (this._selectedNodeList.indexOf(d) !== -1) {
  396. this._selectedNodeList.forEach(function(node) {
  397. node.x += d3.event.dx;
  398. node.y += d3.event.dy;
  399. });
  400. } else {
  401. d.x += d3.event.dx;
  402. d.y += d3.event.dy;
  403. }
  404. this._diagramContentProvider.animateNodeTick(nodes);
  405. this._diagramContentProvider.animateLinkTick(links);
  406. }.bind(this)).on('end', function (d) {
  407. if (this._selectedNodeList.indexOf(d) === -1) {
  408. this._setPosition(d.moserObject, {
  409. x: d.x,
  410. y: d.y
  411. });
  412. } else {
  413. this._selectedNodeList.forEach(function(node) {
  414. this._setPosition(node.moserObject, {
  415. x: node.x,
  416. y: node.y
  417. });
  418. }.bind(this));
  419. }
  420. }.bind(this)));
  421. if (this._withNewNodes && !this._isAnimating) {
  422. this._isAnimating = true;
  423. }
  424. this._d3ForceContext.on('tick', function () {
  425. self._diagramContentProvider.animateNodeTick(nodes);
  426. self._diagramContentProvider.animateLinkTick(links);
  427. nodes.each(function (n) {
  428. if (self._d3ForceContext.alpha() < 0.01) {
  429. n.fx = n.x;
  430. n.fy = n.y;
  431. self._setPosition(n.moserObject, {
  432. x: n.x,
  433. y: n.y
  434. });
  435. self._d3ForceContext.stop();
  436. self._isAnimating = false;
  437. self._withNewNodes = false;
  438. }
  439. });
  440. });
  441. this.redraw();
  442. },
  443. render: function (moserModule) {
  444. if (this._isAnimating) {
  445. this.redraw();
  446. return;
  447. }
  448. this._nodeList = [];
  449. this._linkList = [];
  450. // first add nodes for packages
  451. var diagramNodes = this.diagramStore.diagramNodes;
  452. diagramNodes.packages.forEach(function (p) {
  453. this.addNode(p, {
  454. filter: false,
  455. instanceType: 'package',
  456. isValid: true
  457. }, this.diagramStore.droppedPosition);
  458. }.bind(this));
  459. // then add nodes for query subject not in packages
  460. diagramNodes.querySubjects.forEach(function (qs) {
  461. this.addNode(qs, {
  462. filter: qs.getFilter().length,
  463. instanceType: qs.instanceType ? qs.instanceType.value() : 'copy',
  464. isValid: InternalBridge.validationUtils.isValid(qs),
  465. severityInfo: InternalBridge.validationUtils.getHighestSeverityErrorInfo(qs)
  466. }, this.diagramStore.droppedPosition);
  467. }.bind(this));
  468. // last add links
  469. this._adjacencyList = {};
  470. moserModule.getRelationship().forEach(function (link) {
  471. this.addLink(link);
  472. }.bind(this));
  473. this.diagramStore.setDroppedPosition();
  474. this.diagramStore.setIsDnD(false);
  475. this._update();
  476. },
  477. /**
  478. * Position offset provides some randomness in the dropping of node positions to prevent them from "flying off"
  479. */
  480. _positionOffset: function () {
  481. return Math.floor(500 * (Math.random() - 0.5));
  482. },
  483. _getOffsetPosition: function (nodePosition) {
  484. var zoomAndPan = this.diagramStore.zoomAndPan;
  485. return {
  486. x: (nodePosition.x + this._positionOffset() - zoomAndPan.translate[0]) / zoomAndPan.scale,
  487. y: (nodePosition.y + this._positionOffset() - zoomAndPan.translate[1]) / zoomAndPan.scale
  488. };
  489. },
  490. /**
  491. * Calculates the position of any new nodes that are dropped to the diagram, otherwise they are placed in the middle/
  492. */
  493. _getPosition: function (moserObject, droppedPosition) {
  494. if (this._hasPosition(moserObject)) {
  495. return this.diagramStore.getPosition(moserObject);
  496. }
  497. var defaultPosition = droppedPosition || {
  498. x: (this.$el.width() / 2),
  499. y: (this.$el.height() / 2)
  500. };
  501. return this._getOffsetPosition(defaultPosition);
  502. },
  503. _hasPosition: function (moserObject) {
  504. var position = this.diagramStore.getPosition(moserObject);
  505. return position.x != null && position.y != null;
  506. },
  507. _setPosition: function (moserObject, nodePosition, withOffset) {
  508. this.diagramStore.setPosition(moserObject, withOffset ? this._getOffsetPosition(nodePosition) : nodePosition);
  509. },
  510. /**
  511. * Show the input box on the diagram over the node that
  512. * will be renamed and send changes to model
  513. */
  514. captureRename : function () {
  515. var focusedNode = d3.select(this.$el.find('.mui-diagram-selected')[0]);
  516. // occasionally get context menu without node focus
  517. if (focusedNode.size() === 0 || !this.diagramStore.isRenameActive) {
  518. return;
  519. }
  520. var nodeData;
  521. var nodeList = this._nodeList;
  522. for (var i = 0; i < nodeList.length; i++) {
  523. if (nodeList[i].identifier === focusedNode.node().__data__.identifier) {
  524. nodeData = nodeList[i];
  525. break;
  526. }
  527. }
  528. this._drawInputBoxRename(focusedNode);
  529. if (nodeData) {
  530. // put existing title in input box
  531. this.$inputBox.val(nodeData.label).focus().select();
  532. this._pushRenameToModel(focusedNode, nodeData.moserObject);
  533. }
  534. },
  535. /**
  536. * Find a matching node to call the rename action on
  537. *
  538. * @param identifier
  539. * The identifier of the node that is to be
  540. * renamed
  541. */
  542. searchNodeListRename : function (newName, identifier) {
  543. var self = this;
  544. this._nodeList.forEach(function (node) {
  545. if (node.identifier === identifier) {
  546. self._renameNode(newName, node);
  547. }
  548. });
  549. },
  550. /**
  551. * Rename the actual diagram node
  552. *
  553. * @param newName
  554. * @param node
  555. */
  556. _renameNode : function (newName, node) {
  557. node.setLabel(newName);
  558. this.$el.find('.mui-diagram-node.' + node.identifier + ' text')
  559. .text(newName);
  560. this._shortenLabel(node.identifier);
  561. },
  562. /**
  563. * Capture the new name of the query subject and push
  564. * changes to model and tree
  565. */
  566. _pushRenameToModel : function (focusedNode, moserObject) {
  567. // change text of diagram node and propagate any
  568. // changes to fancytree on focus change
  569. var self = this;
  570. var modelInfo =
  571. {
  572. model: self._diagramContentProvider._model,
  573. node: focusedNode
  574. };
  575. this.$inputBox.blur(modelInfo, function () {
  576. self._onInputBlur(moserObject);
  577. }).keypress(modelInfo, function (e) {
  578. if (e.which === 13) {
  579. self._onInputBlur(moserObject);
  580. }
  581. });
  582. },
  583. _onInputBlur : function (moserObject) {
  584. if (this.$inputBox && this.$inputBox.length > 0) {
  585. this.$inputBox.remove();
  586. this.$imputBox = null;
  587. this.diagramStore.setRenameActive(false);
  588. if (moserObject) {
  589. this.diagramStore.setLabel(moserObject, this.$inputBox.val());
  590. }
  591. }
  592. },
  593. /**
  594. * Draw and size the input box used for diagram rename
  595. * action
  596. */
  597. _drawInputBoxRename : function (focusedNode) {
  598. var node = this.$el.find('.mui-diagram-selected');
  599. var bboxNode = node.find('.mui-diagram-node-box')[0].getBoundingClientRect();
  600. var bboxSVG = this.$el.offset();
  601. var renameLeft = bboxNode.left - bboxSVG.left;
  602. var renameTop = bboxNode.top - bboxSVG.top;
  603. var textSize = focusedNode.select('text').style('font-size')
  604. .replace(/[^-\d\.]/g, '') * this.diagramStore.zoomAndPan.scale;
  605. // change size of input box if filter icon is
  606. // visible
  607. //
  608. // 0.9 = % of node width used for input box when
  609. // filter icon is not present
  610. //
  611. // 0.78 = % of node width used for input box when
  612. // filter icon is present
  613. var widthOffset = 0.9;
  614. if (node[0].__data__.filter) {
  615. widthOffset = 0.78;
  616. }
  617. // add and size the input box based on the zoom
  618. // level
  619. //
  620. // 0.05 = % of node width used to give a buffer on
  621. // left of input box (between edge of node and start
  622. // of the input)
  623. //
  624. // 0.25 = % of node height used to buffer top of
  625. // input box (between top of node and input)
  626. this.$inputBox = $('<input class=\'mui-diagram-rename\' type=\'text\' style=\'position:absolute; left:'
  627. + (renameLeft + bboxNode.width * 0.05)
  628. + 'px; top:'
  629. + (renameTop + bboxNode.height * 0.25)
  630. + 'px; width:'
  631. + (bboxNode.width * widthOffset)
  632. + 'px; height:'
  633. + (bboxNode.height / 2)
  634. + 'px; font-size: '
  635. + textSize + 'px;\'>');
  636. this.$el.find('svg').before(this.$inputBox);
  637. },
  638. /**
  639. * Properly shorten the label for the given identifier's
  640. * node
  641. *
  642. * @param identifier
  643. * A valid querySubject identifier
  644. */
  645. _shortenLabel : function (identifier) {
  646. var node = d3.select(this.$el.find('.mui-diagram-node.' + identifier)[0]);
  647. this.$el.find('.mui-diagram-node.' + identifier + ' text')[0].textContent = StringUtils
  648. .shortenTextMidEllipsis(
  649. node.data()[0].label, StringUtils.getMaxDiagramLabelLength(node.data()[0].filter, node.data()[0].label));
  650. },
  651. /**
  652. * Update the diagram node's styling based on new properties
  653. *
  654. * @param node
  655. * The node
  656. */
  657. updateNodeStyling: function (node) {
  658. if (node) {
  659. // find matching node in diagram node list
  660. var n = _.find(this._nodeList, function (ele) {
  661. return ele.identifier === node.identifier;
  662. });
  663. if (n) {
  664. // set properties on matching diagram node
  665. n.setInstanceType(node.instanceType);
  666. n.setHidden(node.hidden);
  667. // update style on node to reflect new data
  668. this._diagramContentProvider.styleNode(d3.select(this.$el.find('.mui-diagram-node.' + n.identifier)[0]));
  669. }
  670. }
  671. },
  672. updateLinkStyling: function (relationship) {
  673. if (relationship) {
  674. // find matching link in diagram link list
  675. var l = _.find(this._linkList, function (ele) {
  676. return ele.identifier === relationship.identifier;
  677. });
  678. if (l) {
  679. l.setInstanceType(relationship.instanceType);
  680. this._diagramContentProvider.styleLink(d3.select(this.$el.find('.mui-diagram-link.' + l.identifier)[0]));
  681. }
  682. }
  683. },
  684. // To simulate react update
  685. redraw: function () {
  686. var zoomAndPan = this.diagramStore.zoomAndPan;
  687. var transform = d3.zoomTransform(this._SVGContext.node());
  688. transform.k = zoomAndPan.scale;
  689. transform.x = zoomAndPan.translate[0];
  690. transform.y = zoomAndPan.translate[1];
  691. d3.zoom().transform(this._SVGContext, transform);
  692. this._SVGContext.diagramContainer.attr('transform', 'translate(' + zoomAndPan.translate + ') ' + 'scale(' + zoomAndPan.scale + ')');
  693. this.selectNodes();
  694. this.showCardinality(this.diagramStore.isCardinalityVisible);
  695. this.showDegreesOfSeparation(this.diagramStore.degreesOfSeparation);
  696. this.captureRename();
  697. }
  698. });
  699. return ModelDiagramView;
  700. });