UNPKG

@joint/core

Version:

JavaScript diagramming library

886 lines (715 loc) 26.1 kB
import * as util from '../util/index.mjs'; import V from '../V/index.mjs'; import { Rect, Point } from '../g/index.mjs'; import * as Port from '../layout/ports/port.mjs'; import * as PortLabel from '../layout/ports/portLabel.mjs'; var PortData = function(data) { var clonedData = util.cloneDeep(data) || {}; this.ports = []; this.groups = {}; this.portLayoutNamespace = Port; this.portLabelLayoutNamespace = PortLabel; this._init(clonedData); }; PortData.prototype = { getPorts: function() { return this.ports; }, getGroup: function(name) { return this.groups[name] || {}; }, getPortsByGroup: function(groupName) { return this.ports.filter(function(port) { return port.group === groupName; }); }, getGroupPortsMetrics: function(groupName, elBBox) { var group = this.getGroup(groupName); var ports = this.getPortsByGroup(groupName); var groupPosition = group.position || {}; var groupPositionName = groupPosition.name; var namespace = this.portLayoutNamespace; if (!namespace[groupPositionName]) { groupPositionName = 'left'; } var groupArgs = groupPosition.args || {}; var portsArgs = ports.map(function(port) { return port && port.position && port.position.args; }); var groupPortTransformations = namespace[groupPositionName](portsArgs, elBBox, groupArgs); var accumulator = { ports: ports, result: [] }; util.toArray(groupPortTransformations).reduce(function(res, portTransformation, index) { var port = res.ports[index]; res.result.push({ portId: port.id, portTransformation: portTransformation, labelTransformation: this._getPortLabelLayout(port, Point(portTransformation), elBBox), portAttrs: port.attrs, portSize: port.size, labelSize: port.label.size }); return res; }.bind(this), accumulator); return accumulator.result; }, _getPortLabelLayout: function(port, portPosition, elBBox) { var namespace = this.portLabelLayoutNamespace; var labelPosition = port.label.position.name || 'left'; if (namespace[labelPosition]) { return namespace[labelPosition](portPosition, elBBox, port.label.position.args); } return null; }, _init: function(data) { // prepare groups if (util.isObject(data.groups)) { var groups = Object.keys(data.groups); for (var i = 0, n = groups.length; i < n; i++) { var key = groups[i]; this.groups[key] = this._evaluateGroup(data.groups[key]); } } // prepare ports var ports = util.toArray(data.items); for (var j = 0, m = ports.length; j < m; j++) { this.ports.push(this._evaluatePort(ports[j])); } }, _evaluateGroup: function(group) { return util.merge(group, { position: this._getPosition(group.position, true), label: this._getLabel(group, true) }); }, _evaluatePort: function(port) { var evaluated = util.assign({}, port); var group = this.getGroup(port.group); evaluated.markup = evaluated.markup || group.markup; evaluated.attrs = util.merge({}, group.attrs, evaluated.attrs); evaluated.position = this._createPositionNode(group, evaluated); evaluated.label = util.merge({}, group.label, this._getLabel(evaluated)); evaluated.z = this._getZIndex(group, evaluated); evaluated.size = util.assign({}, group.size, evaluated.size); return evaluated; }, _getZIndex: function(group, port) { if (util.isNumber(port.z)) { return port.z; } if (util.isNumber(group.z) || group.z === 'auto') { return group.z; } return 'auto'; }, _createPositionNode: function(group, port) { return util.merge({ name: 'left', args: {} }, group.position, { args: port.args }); }, _getPosition: function(position, setDefault) { var args = {}; var positionName; if (util.isFunction(position)) { positionName = 'fn'; args.fn = position; } else if (util.isString(position)) { positionName = position; } else if (position === undefined) { positionName = setDefault ? 'left' : null; } else if (Array.isArray(position)) { positionName = 'absolute'; args.x = position[0]; args.y = position[1]; } else if (util.isObject(position)) { positionName = position.name; util.assign(args, position.args); } var result = { args: args }; if (positionName) { result.name = positionName; } return result; }, _getLabel: function(item, setDefaults) { var label = item.label || {}; var ret = label; ret.position = this._getPosition(label.position, setDefaults); return ret; } }; export const elementPortPrototype = { _initializePorts: function() { this._createPortData(); this.on('change:ports', function() { this._processRemovedPort(); this._createPortData(); }, this); }, /** * remove links tied wiht just removed element * @private */ _processRemovedPort: function() { var current = this.get('ports') || {}; var currentItemsMap = {}; util.toArray(current.items).forEach(function(item) { currentItemsMap[item.id] = true; }); var previous = this.previous('ports') || {}; var removed = {}; util.toArray(previous.items).forEach(function(item) { if (!currentItemsMap[item.id]) { removed[item.id] = true; } }); var graph = this.graph; if (graph && !util.isEmpty(removed)) { var inboundLinks = graph.getConnectedLinks(this, { inbound: true }); inboundLinks.forEach(function(link) { if (removed[link.get('target').port]) link.remove(); }); var outboundLinks = graph.getConnectedLinks(this, { outbound: true }); outboundLinks.forEach(function(link) { if (removed[link.get('source').port]) link.remove(); }); } }, /** * @returns {boolean} */ hasPorts: function() { var ports = this.prop('ports/items'); return Array.isArray(ports) && ports.length > 0; }, /** * @param {string} id * @returns {boolean} */ hasPort: function(id) { return this.getPortIndex(id) !== -1; }, /** * @returns {Array<object>} */ getPorts: function() { return util.cloneDeep(this.prop('ports/items')) || []; }, /** * @returns {Array<object>} */ getGroupPorts: function(groupName) { const groupPorts = util.toArray(this.prop(['ports','items'])).filter(port => port.group === groupName); return util.cloneDeep(groupPorts); }, /** * @param {string} id * @returns {object} */ getPort: function(id) { return util.cloneDeep(util.toArray(this.prop('ports/items')).find(function(port) { return port.id && port.id === id; })); }, getPortGroupNames: function() { return Object.keys(this._portSettingsData.groups); }, /** * @param {string} groupName * @returns {Object<portId, {x: number, y: number, angle: number}>} */ getPortsPositions: function(groupName) { var portsMetrics = this._portSettingsData.getGroupPortsMetrics(groupName, Rect(this.size())); return portsMetrics.reduce(function(positions, metrics) { var transformation = metrics.portTransformation; positions[metrics.portId] = { x: transformation.x, y: transformation.y, angle: transformation.angle }; return positions; }, {}); }, /** * @param {string|Port} port port id or port * @returns {number} port index */ getPortIndex: function(port) { var id = util.isObject(port) ? port.id : port; if (!this._isValidPortId(id)) { return -1; } return util.toArray(this.prop('ports/items')).findIndex(function(item) { return item.id === id; }); }, /** * @param {object} port * @param {object} [opt] * @returns {joint.dia.Element} */ addPort: function(port, opt) { if (!util.isObject(port) || Array.isArray(port)) { throw new Error('Element: addPort requires an object.'); } var ports = util.assign([], this.prop('ports/items')); ports.push(port); this.prop('ports/items', ports, opt); return this; }, /** * @param {string|Port|number} before * @param {object} port * @param {object} [opt] * @returns {joint.dia.Element} */ insertPort: function(before, port, opt) { const index = (typeof before === 'number') ? before : this.getPortIndex(before); if (!util.isObject(port) || Array.isArray(port)) { throw new Error('dia.Element: insertPort requires an object.'); } const ports = util.assign([], this.prop('ports/items')); ports.splice(index, 0, port); this.prop('ports/items', ports, opt); return this; }, /** * @param {string} portId * @param {string|object=} path * @param {*=} value * @param {object=} opt * @returns {joint.dia.Element} */ portProp: function(portId, path, value, opt) { var index = this.getPortIndex(portId); if (index === -1) { throw new Error('Element: unable to find port with id ' + portId); } var args = Array.prototype.slice.call(arguments, 1); if (Array.isArray(path)) { args[0] = ['ports', 'items', index].concat(path); } else if (util.isString(path)) { // Get/set an attribute by a special path syntax that delimits // nested objects by the colon character. args[0] = ['ports/items/', index, '/', path].join(''); } else { args = ['ports/items/' + index]; if (util.isPlainObject(path)) { args.push(path); args.push(value); } } return this.prop.apply(this, args); }, _validatePorts: function() { var portsAttr = this.get('ports') || {}; var errorMessages = []; portsAttr = portsAttr || {}; var ports = util.toArray(portsAttr.items); ports.forEach(function(p) { if (typeof p !== 'object') { errorMessages.push('Element: invalid port ', p); } if (!this._isValidPortId(p.id)) { p.id = this.generatePortId(); } }, this); if (util.uniq(ports, 'id').length !== ports.length) { errorMessages.push('Element: found id duplicities in ports.'); } return errorMessages; }, generatePortId: function() { return this.generateId(); }, /** * @param {string} id port id * @returns {boolean} * @private */ _isValidPortId: function(id) { return id !== null && id !== undefined && !util.isObject(id); }, addPorts: function(ports, opt) { if (ports.length) { this.prop('ports/items', util.assign([], this.prop('ports/items')).concat(ports), opt); } return this; }, removePort: function(port, opt) { const options = opt || {}; const index = this.getPortIndex(port); if (index !== -1) { const ports = util.assign([], this.prop(['ports', 'items'])); ports.splice(index, 1); options.rewrite = true; this.startBatch('port-remove'); this.prop(['ports', 'items'], ports, options); this.stopBatch('port-remove'); } return this; }, removePorts: function(portsForRemoval, opt) { let options, newPorts; if (Array.isArray(portsForRemoval)) { options = opt || {}; if (portsForRemoval.length === 0) return this.this; const currentPorts = util.assign([], this.prop(['ports', 'items'])); newPorts = currentPorts.filter(function(cp) { return !portsForRemoval.some(function(rp) { const rpId = util.isObject(rp) ? rp.id : rp; return cp.id === rpId; }); }); } else { options = portsForRemoval || {}; newPorts = []; } this.startBatch('port-remove'); options.rewrite = true; this.prop(['ports', 'items'], newPorts, options); this.stopBatch('port-remove'); return this; }, /** * @private */ _createPortData: function() { var err = this._validatePorts(); if (err.length > 0) { this.set('ports', this.previous('ports')); throw new Error(err.join(' ')); } var prevPortData; if (this._portSettingsData) { prevPortData = this._portSettingsData.getPorts(); } this._portSettingsData = new PortData(this.get('ports')); var curPortData = this._portSettingsData.getPorts(); if (prevPortData) { var added = curPortData.filter(function(item) { if (!prevPortData.find(function(prevPort) { return prevPort.id === item.id; })) { return item; } }); var removed = prevPortData.filter(function(item) { if (!curPortData.find(function(curPort) { return curPort.id === item.id; })) { return item; } }); if (removed.length > 0) { this.trigger('ports:remove', this, removed); } if (added.length > 0) { this.trigger('ports:add', this, added); } } } }; export const elementViewPortPrototype = { portContainerMarkup: 'g', portMarkup: [{ tagName: 'circle', selector: 'circle', attributes: { 'r': 10, 'fill': '#FFFFFF', 'stroke': '#000000' } }], portLabelMarkup: [{ tagName: 'text', selector: 'text', attributes: { 'fill': '#000000' } }], /** @type {Object<string, {portElement: Vectorizer, portLabelElement: Vectorizer}>} */ _portElementsCache: null, /** * @private */ _initializePorts: function() { this._cleanPortsCache(); }, /** * @typedef {Object} Port * * @property {string} id * @property {Object} position * @property {Object} label * @property {Object} attrs * @property {string} markup * @property {string} group */ /** * @private */ _refreshPorts: function() { this._removePorts(); this._cleanPortsCache(); this._renderPorts(); }, _cleanPortsCache: function() { this._portElementsCache = {}; }, /** * @private */ _renderPorts: function() { // references to rendered elements without z-index var elementReferences = []; var elem = this._getContainerElement(); for (var i = 0, count = elem.node.childNodes.length; i < count; i++) { elementReferences.push(elem.node.childNodes[i]); } var portsGropsByZ = util.groupBy(this.model._portSettingsData.getPorts(), 'z'); var withoutZKey = 'auto'; // render non-z first util.toArray(portsGropsByZ[withoutZKey]).forEach(function(port) { var portElement = this._getPortElement(port); elem.append(portElement); elementReferences.push(portElement); }, this); var groupNames = Object.keys(portsGropsByZ); for (var k = 0; k < groupNames.length; k++) { var groupName = groupNames[k]; if (groupName !== withoutZKey) { var z = parseInt(groupName, 10); this._appendPorts(portsGropsByZ[groupName], z, elementReferences); } } this._updatePorts(); }, /** * @returns {V} * @private */ _getContainerElement: function() { return this.rotatableNode || this.vel; }, /** * @param {Array<Port>}ports * @param {number} z * @param refs * @private */ _appendPorts: function(ports, z, refs) { var containerElement = this._getContainerElement(); var portElements = util.toArray(ports).map(this._getPortElement, this); if (refs[z] || z < 0) { V(refs[Math.max(z, 0)]).before(portElements); } else { containerElement.append(portElements); } }, /** * Try to get element from cache, * @param port * @returns {*} * @private */ _getPortElement: function(port) { if (this._portElementsCache[port.id]) { return this._portElementsCache[port.id].portElement; } return this._createPortElement(port); }, findPortNodes: function(portId, selector) { const portCache = this._portElementsCache[portId]; if (!portCache) return []; if (!selector) return [portCache.portContentElement.node]; const portRoot = portCache.portElement.node; const portSelectors = portCache.portSelectors; return this.findBySelector(selector, portRoot, portSelectors); }, findPortNode: function(portId, selector) { const [node = null] = this.findPortNodes(portId, selector); return node; }, /** * @private */ _updatePorts: function() { // layout ports without group this._updatePortGroup(undefined); // layout ports with explicit group var groupsNames = Object.keys(this.model._portSettingsData.groups); groupsNames.forEach(this._updatePortGroup, this); }, /** * @private */ _removePorts: function() { util.invoke(this._portElementsCache, 'portElement.remove'); }, /** * @param {Port} port * @returns {V} * @private */ _createPortElement: function(port) { let portElement; let labelElement; let labelSelectors; let portSelectors; var portContainerElement = V(this.portContainerMarkup).addClass('joint-port'); var portMarkup = this._getPortMarkup(port); if (Array.isArray(portMarkup)) { var portDoc = this.parseDOMJSON(portMarkup, portContainerElement.node); var portFragment = portDoc.fragment; if (portFragment.childNodes.length > 1) { portElement = V('g').append(portFragment); } else { portElement = V(portFragment.firstChild); } portSelectors = portDoc.selectors; } else { portElement = V(portMarkup); if (Array.isArray(portElement)) { portElement = V('g').append(portElement); } } if (!portElement) { throw new Error('ElementView: Invalid port markup.'); } portElement.attr({ 'port': port.id, 'port-group': port.group }); // If the port ID is a number, we need to add // extra information to the port element to distinguish // between ports with the same ID but different types. if (util.isNumber(port.id)) { portElement.attr('port-id-type', 'number'); } const labelMarkupDef = this._getPortLabelMarkup(port.label); if (Array.isArray(labelMarkupDef)) { // JSON Markup const { fragment, selectors } = this.parseDOMJSON(labelMarkupDef, portContainerElement.node); const childCount = fragment.childNodes.length; if (childCount > 0) { labelSelectors = selectors; labelElement = (childCount === 1) ? V(fragment.firstChild) : V('g').append(fragment); } } else { // String Markup labelElement = V(labelMarkupDef); if (Array.isArray(labelElement)) { labelElement = V('g').append(labelElement); } } var portContainerSelectors; if (portSelectors && labelSelectors) { for (var key in labelSelectors) { if (portSelectors[key] && key !== this.selector) throw new Error('ElementView: selectors within port must be unique.'); } portContainerSelectors = util.assign({}, portSelectors, labelSelectors); } else { portContainerSelectors = portSelectors || labelSelectors || {}; } // The `portRootSelector` points to the root SVGNode of the port. // Either the implicit wrapping group <g/> in case the port consist of multiple SVGNodes. // Or the single SVGNode of the port. const portRootSelector = 'portRoot'; // The `labelRootSelector` points to the root SVGNode of the label. const labelRootSelector = 'labelRoot'; // The `labelTextSelector` points to all text SVGNodes of the label. const labelTextSelector = 'labelText'; if (!(portRootSelector in portContainerSelectors)) { portContainerSelectors[portRootSelector] = portElement.node; } if (labelElement) { const labelNode = labelElement.node; if (!(labelRootSelector in portContainerSelectors)) { portContainerSelectors[labelRootSelector] = labelNode; } if (!(labelTextSelector in portContainerSelectors)) { // If the label is a <text> element, we can use it directly. // Otherwise, we need to find the <text> element within the label. const labelTextNode = (labelElement.tagName() === 'TEXT') ? labelNode : Array.from(labelNode.querySelectorAll('text')); portContainerSelectors[labelTextSelector] = labelTextNode; if (!labelSelectors) labelSelectors = {}; labelSelectors[labelTextSelector] = labelTextNode; } } portContainerElement.append(portElement.addClass('joint-port-body')); if (labelElement) { portContainerElement.append(labelElement.addClass('joint-port-label')); } this._portElementsCache[port.id] = { portElement: portContainerElement, portLabelElement: labelElement, portSelectors: portContainerSelectors, portLabelSelectors: labelSelectors, portContentElement: portElement, portContentSelectors: portSelectors }; return portContainerElement; }, /** * @param {string=} groupName * @private */ _updatePortGroup: function(groupName) { var elementBBox = Rect(this.model.size()); var portsMetrics = this.model._portSettingsData.getGroupPortsMetrics(groupName, elementBBox); for (var i = 0, n = portsMetrics.length; i < n; i++) { var metrics = portsMetrics[i]; var portId = metrics.portId; var cached = this._portElementsCache[portId] || {}; var portTransformation = metrics.portTransformation; var labelTransformation = metrics.labelTransformation; if (labelTransformation && cached.portLabelElement) { this.updateDOMSubtreeAttributes(cached.portLabelElement.node, labelTransformation.attrs, { rootBBox: new Rect(metrics.labelSize), selectors: cached.portLabelSelectors }); this.applyPortTransform(cached.portLabelElement, labelTransformation, (-portTransformation.angle || 0)); } this.updateDOMSubtreeAttributes(cached.portElement.node, metrics.portAttrs, { rootBBox: new Rect(metrics.portSize), selectors: cached.portSelectors }); this.applyPortTransform(cached.portElement, portTransformation); } }, /** * @param {Vectorizer} element * @param {{dx:number, dy:number, angle: number, attrs: Object, x:number: y:number}} transformData * @param {number=} initialAngle * @constructor */ applyPortTransform: function(element, transformData, initialAngle) { var matrix = V.createSVGMatrix() .rotate(initialAngle || 0) .translate(transformData.x || 0, transformData.y || 0) .rotate(transformData.angle || 0); element.transform(matrix, { absolute: true }); }, /** * @param {Port} port * @returns {string} * @private */ _getPortMarkup: function(port) { return port.markup || this.model.get('portMarkup') || this.model.portMarkup || this.portMarkup; }, /** * @param {Object} label * @returns {string} * @private */ _getPortLabelMarkup: function(label) { return label.markup || this.model.get('portLabelMarkup') || this.model.portLabelMarkup || this.portLabelMarkup; } };