/** * Licensed Materials - Property of IBM IBM Cognos Products: Modeling UI (C) Copyright IBM Corp. 2015, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP * Schedule Contract with IBM Corp. */ define([ 'jquery', 'underscore', 'bi/commons/ui/core/Class', 'bi/commons/utils/Utils', '../ui/JoinPopupView', './StringUtils', './NodeStylesUtils', './utils', 'bi/commons/utils/BidiUtil', '../../StringResourcesBridge', ], function($, _, Class, CommonUtils, JoinPopupView, StringUtils, NodeStylesUtils, utils, BidiUtil, resources) { var d3; var DiagramContentProvider = Class.extend({ CommonUtils: CommonUtils, init: function(options) { DiagramContentProvider.inherited('init', this); _.extend(this, options); d3 = options.d3; this._dblClicked = false; }, setModel: function(model) { this._model = model; }, getModel: function(){ return this._model; }, /** * Will draw links in the diagram based on this definition * * @param {array} * links - A d3 array of links in a force diagram */ drawLinks: function(SVGContext, links) { var d3Link = SVGContext.diagramContainer.selectAll('.mui-diagram-link') .data(links, function(d) { return [d.identifier, d.instanceType, d.isValid, d.isLeftMincardZero, d.isRightMincardZero, d.sourceCardinality, d.targetCardinality]; }); // exit old links d3Link.exit().remove(); var link = d3Link.enter().insert('g').attr( 'class', function(d) { var referenceClass = d.instanceType === 'reference' ? 'mui-reference-link ' : ''; return 'mui-diagram-link ' + referenceClass + d.source.identifier + ' ' + d.target.identifier + ' ' + d.identifier; }); link.append('line').attr('class', 'mui-diagram-link-interact-line'); link.append('line').attr('class', 'mui-diagram-link-display-line'); // validation icon var validationIconGroup = utils.d3Attrs(link.append('g'), NodeStylesUtils.diagramNodeStyles.validationLinkGroupIconStyle).on('click', function(data) { d3.event.stopPropagation(); if (data.validationCallback) { data.validationCallback(d3.event); } }); utils.d3Attrs(validationIconGroup.append('use'), NodeStylesUtils.diagramNodeStyles.validationLinkIconStyle); utils.d3Attrs(validationIconGroup.append('circle'), NodeStylesUtils.diagramNodeStyles.validationLinkCircleStyle); // group for link cardinality var linkCardinality = link.append('g').attr('class', 'mui-diagram-cardinality'); this._drawLinkCardinalityNotation(linkCardinality, true); this._drawLinkCardinalityNotation(linkCardinality, false); this._attachLinkActions(link); // update links this._updateLinkProperties(link); this.animateLinkTick(link); return link.merge(d3Link); }, /** * Update any existing properties on the links * * @param {array} * links - A d3 array of links in a force diagram */ _updateLinkProperties: function(links) { links.each(function(d) { var node = d3.select(this); var representedLink = d.linksRepresented && d.linksRepresented[0] || {}; // update cardinality source and target labels node.select('.mui-diagram-cardinality-source').classed('hidden', d.hideCardinality).classed('zero', representedLink.isLeftMincardZero).select('text') .text(d.sourceCardinality); node.select('.mui-diagram-cardinality-target').classed('hidden', d.hideCardinality).classed('zero', representedLink.isRightMincardZero).select('text') .text(d.targetCardinality); }); }, /** * A private method for styling cardinality markers * @param {SVG} linkGroup - an SVG element representing a link * @param {boolean} isSource - Indicates which end of the link a cardinality value belongs to ('true' is the source, 'false' is the target) */ _drawLinkCardinalityNotation: function(linkGroup, isSource) { // group for cardinality var cardinalityGroup = linkGroup.append('g').attr('class', isSource ? 'mui-diagram-cardinality-source' : 'mui-diagram-cardinality-target'); // rect styled as a circle // ... need to add the attributes directly on the node // as not all SVG element attributes can be styled with CSS // see: http://www.w3.org/TR/SVG/propidx.html utils.d3Attrs(cardinalityGroup.append('rect'), NodeStylesUtils.CardinalityStyle.boxStyle); // the cardinality relation text utils.d3Attrs(cardinalityGroup.append('text'), NodeStylesUtils.CardinalityStyle.textStyle); }, /** * Will draw nodes in the diagram based on this definition * * @param {array} * d3Nodes - A d3 array of nodes in a force diagram */ drawNodes: function(SVGContext, nodes) { var d3Node = SVGContext.diagramContainer.selectAll('.mui-diagram-node') .data(nodes, function(d) { return [d.tableRowCount, d.displayTableInfo, d.identifier, d.label, d.screenTip, d.filter, d.hidden, d.instanceType, d.isValid]; }); d3Node.exit().remove(); var box = d3Node.enter().insert('g').attr( 'class', function(d) { return 'mui-diagram-node ' + d.identifier; }); box.append('rect').attr('class', 'mui-diagram-node-box').append('title').text(function (d) { return BidiUtil.enforceTextDirection(d.screenTip ? (d.screenTip + ' : ' + d.label) : d.label); }); var boxContent = box.append('g').attr('class', function(d) { return 'mui-diagram-node-content ' + d.identifier; }); var validationIconGroup = boxContent.append('g').on('mousedown', function (data) { d3.event.stopPropagation(); if (data.validationCallback) { data.validationCallback(d3.event); } }); utils.d3Attrs(validationIconGroup.append('use'), NodeStylesUtils.diagramNodeStyles.validationNodeIconStyle); utils.d3Attrs(validationIconGroup.append('circle'), NodeStylesUtils.diagramNodeStyles.validationNodeCircleStyle); // text var self = this; boxContent.append('text').text(function(d) { var isIconPresent = this.__data__.filter || d.instanceType === 'reference' || d.instanceType === 'package' || !d.isValid; return BidiUtil.enforceTextDirection(StringUtils.shortenTextMidEllipsis(d.label, StringUtils.getMaxDiagramLabelLength(isIconPresent, d.label))); }); // filter icon boxContent.append('use').attr('class', 'mui-diagram-filter'); // reference icon boxContent.append('use').attr('class', 'mui-daigram-reference'); // table details var boxTableDetailsContent = box.append('g').attr('class', function(d) { return 'mui-diagram-table-details ' + d.identifier; }); boxTableDetailsContent.append('rect'); boxTableDetailsContent.append('text').text(function(d) { return BidiUtil.enforceTextDirection(d.statisticsLoading ? resources.get('moduleRowCount', { rowCount: '--' }) : d.tableRowCount); }); // style it this.styleNode(box); this._attachNodeActions(box); this.animateNodeTick(box); return box.merge(d3Node); }, styleNode: function(box) { // rect utils.d3Attrs(box.select('.mui-diagram-node-box'), NodeStylesUtils.diagramNodeStyles.boxStyle); utils.d3Styles(box.select('.mui-diagram-node-box'), NodeStylesUtils.diagramNodeStyles.boxColorStyle); // text utils.d3Attrs(box.select('.mui-diagram-node-content').select('text'), NodeStylesUtils.diagramNodeStyles.textStyle); // filter icon utils.d3Attrs(box.select('.mui-diagram-filter'), NodeStylesUtils.diagramNodeStyles.filterIconStyle); // reference icon utils.d3Attrs(box.select('.mui-diagram-reference'), NodeStylesUtils.diagramNodeStyles.referenceIconStyle); // table details box utils.d3Attrs(box.select('.mui-diagram-table-details').select('rect'), NodeStylesUtils.diagramNodeStyles.tableDetailsBoxStyle); utils.d3Attrs(box.select('.mui-diagram-table-details').select('text'), NodeStylesUtils.diagramNodeStyles.tableDetailsTextStyle); }, styleLink: function(box) { box.classed('mui-reference-link', function(d) { return d.instanceType === 'reference'; }); }, _attachLinkActions: function(links) { var currentModel = this._model; var position = {}; var handle; links.on( 'contextmenu', function(selectedLink, i, g){ var event = d3.event; event.preventDefault(); if (!utils.isOffset(position, {x: event.clientX, y: event.clientY})) { this.diagramStore.showContextMenu(event.pageX, event.pageY, [selectedLink.moserObject]); } return false; }.bind(this)); links.on( 'click', function(selectedObjects, i, g){ var event = d3.event; event.stopPropagation(); this.diagramStore.hideContextMenu(); setTimeout(function() { if (this._dblClicked) { return; } $(this.el).find('.mui_JoinPopup ').remove(); var rect = this.el.getBoundingClientRect(); var popupView = new JoinPopupView({ relations: selectedObjects.linksRepresented, source: selectedObjects.source, target: selectedObjects.target, position: [event.clientX - rect.left, event.clientY - rect.top], $el: $(this.el), model: currentModel }); popupView.render(); }.bind(this), 300); }.bind(this)); links.on( 'dblclick', function(selectedLink, i, g) { var event = d3.event; this._dblClicked = true; event.stopPropagation(); this.diagramStore.editRelationship(selectedLink.moserObject).then(function() { this._dblClicked = false; }.bind(this)); return false; }.bind(this)); }, handleDiagramNodeSelection: function(e){ var isMultiSelect = this.CommonUtils.isControlKey(d3.event); var isRightClick = (d3.event.button === 2); var target = d3.select(d3.event.currentTarget); var targetIsSelected = target.classed('mui-diagram-selected'); var targetIdentifier = e.identifier; var targetMoserObj = e.moserObject; if (isMultiSelect) { //don't de-select on a right click -> context menu will appear over unselected node if (!isRightClick || !targetIsSelected) { d3.select(d3.event.target.parentNode).classed('mui-diagram-selected', function(){ return !d3.select(this).classed('mui-diagram-selected'); }); } if (target.classed('mui-diagram-selected')) { this.diagramStore.appendToSelection(targetMoserObj); } else { this.diagramStore.removeFromSelection(targetMoserObj); } //single click } else { //On sinle-click, deselect all other nodes than the target node. //on a right click where the target is already selected, do nothing if(!isRightClick || !targetIsSelected) { d3.select(d3.event.target.parentNode).classed('mui-diagram-selected', true); this.diagramStore.setSelection([targetMoserObj]); } } }, /** * Defines action handlers for specific inputs and attaches them to actions on a node * * @param {array} * nodes - An array of d3 nodes in the diagram */ _attachNodeActions: function(nodes) { var position = {}; nodes.on( 'contextmenu', function(selectedNode, i, g) { var event = d3.event; event.preventDefault(); if (!utils.isOffset(position, {x: event.clientX, y: event.clientY})) { var rect = this.el.getBoundingClientRect(); this.handleDiagramNodeSelection(event.currentTarget.__data__); this.diagramStore.showContextMenu(event.pageX, event.pageY, undefined, { x: event.pageX - rect.left, y: event.pageY - rect.top }, selectedNode.moserObject); } return false; }.bind(this)); nodes.on( 'click', function(selectedNode, i, g) { var event = d3.event; event.stopPropagation(); !utils.isOffset(position, {x: event.clientX, y: event.clientY}) && this.handleDiagramNodeSelection(event.currentTarget.__data__); }.bind(this)); nodes.on( 'mousedown', function(selectedNode, i, g) { var event = d3.event; position.x = event.clientX; position.y = event.clientY; }); }, /** * Defines how to animate links on a d3 'tick' event * * @param {array} * link - A d3 array of links in the diagram view */ animateLinkTick: function(link) { var lineDefinition = { x1: function(linkData) { return (linkData.source.x); }, x2: function(linkData) { return (linkData.target.x); }, y1: function(linkData) { var tableDetailsOffset = NodeStylesUtils.diagramNodeStyles.tableDetailsBoxStyle.height / 2, source = linkData.source; return (source.y + (source.displayTableInfo ? tableDetailsOffset : 0)); }, y2: function(linkData) { var tableDetailsOffset = NodeStylesUtils.diagramNodeStyles.tableDetailsBoxStyle.height / 2, target = linkData.target; return (target.y + (target.displayTableInfo ? tableDetailsOffset : 0)); } }; utils.d3Attrs(link.select('.mui-diagram-link-display-line'), lineDefinition); utils.d3Attrs(link.select('.mui-diagram-link-interact-line'), lineDefinition); link.select('.mui_validationIcon').attr('transform', function(d) { return 'translate('+ (((d.source.x + d.target.x ) / 2 ) - 6) + ',' + ((( d.source.y + d.target.y ) / 2 ) - 6) + ')'; }); link.select('.mui-diagram-cardinality-source').attr('transform', function(linkData) { var coords = utils.getCardinalityPosition(linkData, 'source'); return 'translate(' + coords.x + ',' + coords.y + ')'; }); link.select('.mui-diagram-cardinality-target').attr('transform', function(linkData) { var coords = utils.getCardinalityPosition(linkData, 'target'); return 'translate(' + coords.x + ',' + coords.y + ')'; }); return link; }, /** * Defines how to animate nodes on a d3 'tick' event * * @param {array} * node - A d3 array of nodes in the diagram view */ animateNodeTick: function(node) { return node.attr('transform', function(d) { return 'translate(' + d.x + ', ' + d.y + ')'; }); } }); return DiagramContentProvider; });