/* ======================================================================== * ZUI: flowChart.js * http://zui.sexy * ======================================================================== * Copyright (c) 2017-2019 cnezsoft.com; Licensed MIT * ======================================================================== */ // https://tc39.github.io/ecma262/#sec-array.prototype.findIndex if (!Array.prototype.findIndex) { Object.defineProperty(Array.prototype, 'findIndex', { value: function (predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw new TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while (k < len) { // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). // d. If testResult is true, return k. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return k; } // e. Increase k by 1. k++; } // 7. Return -1. return -1; } }); } // https://tc39.github.io/ecma262/#sec-array.prototype.find if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { value: function (predicate) { // 1. Let O be ? ToObject(this value). if (this == null) { throw new TypeError('"this" is null or not defined'); } var o = Object(this); // 2. Let len be ? ToLength(? Get(O, "length")). var len = o.length >>> 0; // 3. If IsCallable(predicate) is false, throw a TypeError exception. if (typeof predicate !== 'function') { throw new TypeError('predicate must be a function'); } // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. var thisArg = arguments[1]; // 5. Let k be 0. var k = 0; // 6. Repeat, while k < len while (k < len) { // a. Let Pk be ! ToString(k). // b. Let kValue be ? Get(O, Pk). // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). // d. If testResult is true, return kValue. var kValue = o[k]; if (predicate.call(thisArg, kValue, k, o)) { return kValue; } // e. Increase k by 1. k++; } // 7. Return undefined. return undefined; } }); } // Production steps of ECMA-262, Edition 5, 15.4.4.18 // Reference: http://es5.github.io/#x15.4.4.18 if (!Array.prototype.forEach) { Array.prototype.forEach = function (callback, thisArg) { var T, k; if (this == null) { throw new TypeError(' this is null or not defined'); } // 1. Let O be the result of calling toObject() passing the // |this| value as the argument. var O = Object(this); // 2. Let lenValue be the result of calling the Get() internal // method of O with the argument "length". // 3. Let len be toUint32(lenValue). var len = O.length >>> 0; // 4. If isCallable(callback) is false, throw a TypeError exception. // See: http://es5.github.com/#x9.11 if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); } // 5. If thisArg was supplied, let T be thisArg; else let // T be undefined. if (arguments.length > 1) { T = thisArg; } // 6. Let k be 0 k = 0; // 7. Repeat, while k < len while (k < len) { var kValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the HasProperty // internal method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { // i. Let kValue be the result of calling the Get internal // method of O with argument Pk. kValue = O[k]; // ii. Call the Call internal method of callback with T as // the this value and argument list containing kValue, k, and O. callback.call(T, kValue, k, O); } // d. Increase k by 1. k++; } // 8. return undefined }; } // Production steps of ECMA-262, Edition 5, 15.4.4.19 // Reference: http://es5.github.io/#x15.4.4.19 if (!Array.prototype.map) { Array.prototype.map = function (callback/*, thisArg*/) { var T, A, k; if (this == null) { throw new TypeError('this is null or not defined'); } // 1. Let O be the result of calling ToObject passing the |this| // value as the argument. var O = Object(this); // 2. Let lenValue be the result of calling the Get internal // method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; // 4. If IsCallable(callback) is false, throw a TypeError exception. // See: http://es5.github.com/#x9.11 if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. if (arguments.length > 1) { T = arguments[1]; } // 6. Let A be a new array created as if by the expression new Array(len) // where Array is the standard built-in constructor with that name and // len is the value of len. A = new Array(len); // 7. Let k be 0 k = 0; // 8. Repeat, while k < len while (k < len) { var kValue, mappedValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the HasProperty internal // method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { // i. Let kValue be the result of calling the Get internal // method of O with argument Pk. kValue = O[k]; // ii. Let mappedValue be the result of calling the Call internal // method of callback with T as the this value and argument // list containing kValue, k, and O. mappedValue = callback.call(T, kValue, k, O); // iii. Call the DefineOwnProperty internal method of A with arguments // Pk, Property Descriptor // { Value: mappedValue, // Writable: true, // Enumerable: true, // Configurable: true }, // and false. // In browsers that support Object.defineProperty, use the following: // Object.defineProperty(A, k, { // value: mappedValue, // writable: true, // enumerable: true, // configurable: true // }); // For best browser support, use the following: A[k] = mappedValue; } // d. Increase k by 1. k++; } // 9. return A return A; }; } (function($) { 'use strict'; var selectText = function($e) { var doc = document; var element = $e[0], range; if(doc.body.createTextRange) { range = document.body.createTextRange(); range.moveToElementText(element); range.select(); } else if(window.getSelection) { var selection = window.getSelection(); range = document.createRange(); range.selectNodeContents(element); selection.removeAllRanges(); selection.addRange(range); } }; var NAME = 'zui.flowChart'; // model name var supportElementTypes = { relation: 'relation', action: 'action', start: 'start', end: 'end', result: 'result', }; // var supportElementProps = { // id: 'string', // type: 'string', // text: 'string', // end: 'string', // order: 'number', // from: 'string', // to: 'string', // position: 'object', // style: 'object', // className: 'string', // }; var idSeed = 1; var FlowChartElement = function(props) { props = $.extend({}, props); var that = this; that.type = supportElementTypes[props.type] || supportElementTypes.action; that.isRelation = that.type === 'relation'; that.isNode = !that.isRelation; that.text = props.text; that.order = props.order || idSeed++; var id = props.id; if (that.isRelation) { that.from = props.from; that.to = props.to; that.id = id || (that.from + '-' + that.to); } else { that.id = id || $.zui.uuid(); } if (props.position && typeof props.position.left === 'number' && typeof props.position.top === 'number') { that.position = { left: props.position.left, top: props.position.top, }; that.customPos = true; } that.order = that.idSeed++; if (that.isRelation) { that.order += 1000000; } if (props.style !== undefined) { that.style = props.style; } if (props.className !== undefined) { that.className = props.className; } }; FlowChartElement.create = function(props) { if (props instanceof FlowChartElement) { return [props]; } var element = new FlowChartElement(props); var elements = [element]; if (element.isNode) { if (props.from) { var froms = $.isArray(props.from) ? props.from : [props.from]; froms.forEach(function(from) { var fromInfo = from.split(':'); elements.push(new FlowChartElement({ type: 'relation', from: fromInfo[0], to: element.id, text: fromInfo[1], id: fromInfo[0] + '-' + element.id })); }); } if (props.to) { var tos = $.isArray(props.to) ? props.to : [props.to]; $.each(tos, function(_, to) { var toInfo = to.split(':'); elements.push({ type: 'relation', to: toInfo[0], text: toInfo[1], from: element.id, id: element.id + '-' + toInfo[0] }); }); } } return elements; }; // The flowChart model class var FlowChart = function(element, options) { var that = this; that.name = NAME; that.idSeed = 0; // Get container var $container = that.$container = $(element).addClass('scrollbar-hover') .css('overflow', 'auto'); that.id = $container.attr('id') || 'flowchart_' + $.zui.uuid(); // Get options options = that.options = $.extend( {}, FlowChart.DEFAULTS, $container.data(), options ); var allLangs = $.extend({}, FlowChart.LANGS, options.langs); var userLang = options.lang || ($.zui && $.zui.clientLang ? $.zui.clientLang() : 'en'); that.lang = allLangs[userLang.replace('_', '-')] || allLangs.en; // Create canvas var $canvas = $container.children('.flowchart-canvas'); if (!$canvas.length) { $canvas = $('
') .appendTo($container); } var canvasID = 'flowchart-canvas-' + that.id; $canvas.attr('id', canvasID); that.$canvas = $canvas; // Reset containers size if (options.width !== undefined) { $container.css('width', options.width); } if (options.height !== undefined) { $container.css('height', options.height); } // Init draggable event that.draggableEnable = options.draggable && $.fn.draggable; if (that.draggableEnable) { var handleDrag = function(e) { that.setNodePosition($(e.element).data('id'), e.pos); }; $canvas.draggable({ container: '#' + canvasID, selector: '.flowchart-node', stopPropagation: true, drag: handleDrag, finish: handleDrag, mouseButton: 'left', }); } // Render that.elements = {}; that.update(options.data); // Init edit event if (options.doubleClickToEdit) { $canvas.on('dblclick', '.flowchart-node,.flowchart-relation', function(e) { var $node = $(this).closest('.flowchart-node,.flowchart-relation'); that.enterEditMode($node.data('id')); e.preventDefault(); }); } // Init contextmenu if (options.showContextMenu && $.zui.ContextMenu) { $canvas.on('mousedown', '.flowchart-node,.flowchart-relation', function(e) { if (e.button === 2) { if (that.showContextMenu($(this).data('id'), e)) { } } e.preventDefault(); e.returnValue = false; }).on('contextmenu', function(e) { e.preventDefault(); e.returnValue = false; return false; }); } // Init quick add if (options.quickAdd) { $canvas.on('mouseenter', '.flowchart-node', function() { that.showQuickAdd($(this).data('id')); }).on('mouseleave', '.flowchart-node', function() { that.hideQuickAdd(); }).on('click', '.flowchart-quick-mark', function () { var $mark = $(this); that.quickAddNode($mark.closest('.flowchart-node').data('id'), $mark.data('direction')); }); } }; // Show quick add marks FlowChart.prototype.showQuickAdd = function(node) { var that = this; if (typeof node === 'string') { node = that.elements[node]; } if (!node) { return; } var $marks = node.$ele.find('.flowchart-quick-marks'); if (!$marks.length) { $marks = $([ '
', '
', '
', '
', '
', '
' ].join('')).appendTo(node.$ele); } $marks.show().children().show(); var options = that.options; if (node.bounds.left < (options.padding + 80 + options.horzSpace)) { $marks.find('[data-direction="left"]').hide(); } if (node.bounds.top < (options.padding + options.nodeHeight + options.vertSpace)) { $marks.find('[data-direction="top"]').hide(); } that._currentQuickAddNode = node; }; // Hide quick add marks FlowChart.prototype.hideQuickAdd = function() { var that = this; var node = that._currentQuickAddNode; if (!node) { return; } node.$ele.find('.flowchart-quick-marks').hide(); that._currentQuickAddNode = null; }; // Quick add node FlowChart.prototype.quickAddNode = function(fromNode, direction) { var that = this; if (typeof fromNode === 'string') { fromNode = that.elements[fromNode]; } if (!fromNode) { return; } var nodeID = $.zui.uuid(); that.update({ id: nodeID, text: '', direction: direction, directionFrom: fromNode, from: fromNode.id, }); that.enterEditMode(nodeID); }; // Show contextmenu FlowChart.prototype.showContextMenu = function(ele, event) { var that = this; if (typeof ele === 'string') { ele = that.elements[ele]; } if (!ele) { return; } var items = [{ id: 'type', html: [ '
', '' + that.lang.type + '', ['action', 'result', 'start', 'stop'].map(function(nodeType) { return '' + that.lang['type.' + nodeType] + ''; }).join(''), '
' ].join(''), onClick: function(e) { var $btn = $(e.target).closest('.btn'); var type = $btn.data('type'); if (type !== ele.type) { ele.type = type; that.render(ele); } } }, { id: 'edit', label: that.lang.edit, onClick: function() { that.enterEditMode(ele); }, }, { id: 'delete', label: that.lang.delete, onClick: function() { if (!that.options.deleteConfirm || confirm(that.lang.confirmToDelete.format(ele.text || ele.id))) { that.delete(ele.id); } } }]; if (typeof that.options.showContextMenu === 'function') { items = that.options.showContextMenu(ele, items, event); } if (items && items.length) { $.zui.ContextMenu.show(items, {event: event}); return true; } }; // Render node FlowChart.prototype.renderNode = function(node, skipLayout) { var that = this; var options = that.options; var $node = $(that._getDomID(node)); if (!$node.length) { $node = $('
').appendTo(that.$canvas); if (that.draggableEnable) { $node.css('cursor', 'move'); } } $node.addClass(node.className); var $text = $node.find('.text'); $text.css($.extend({ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', }, options.nodeTextStyle)).text((node.text === undefined || node.text === null) ? '' : node.text); node.$ele = $node; if (node.position && !node.customPos) { node.position = null; } var style = $.extend({ display: 'flex', justifyContent: 'center', alignItems: 'center', maxHeight: options.nodeHeight, border: '1px solid #333', maxWidth: 200, minWidth: 70, background: 'white', padding: "9px 10px", }, options.nodeStyle, options[node.type + 'NodeStyle'], node.style, node.position, { position: 'absolute', visibility: node.position ? 'visible' : 'hidden', }); $node.css(style); var size = { width: $node.outerWidth(), height: $node.outerHeight(), }; var adsorptionGrid = that.options.adsorptionGrid; if (adsorptionGrid === true) { adsorptionGrid = 5; } size.width = Math.ceil(size.width / (adsorptionGrid * 2)) * adsorptionGrid * 2; // size.height = Math.ceil(size.height / (adsorptionGrid)) * adsorptionGrid; node.size = size; if (!skipLayout) { this._layoutNode(node); } if (!options.mergeSideLines) { node.sideRels = { top: [], right: [], bottom: [], left: [], }; } }; // Caculate node position and change layout of it. FlowChart.prototype._layoutNode = function(node, skipPosition) { var that = this; var size = node.size; var position = node.position; if (!position || !node.customPos) { var options = that.options; var parents = node.parents; var direction = node.direction; var directionFrom = node.directionFrom; position = {}; var supportDirections = {top: 1, left: 1, bottom: 1, right: 1}; if (direction && supportDirections[direction] && directionFrom) { var fromBounds = directionFrom.bounds; if (direction === 'top') { position.left = Math.floor(fromBounds.centerLeft - (size.width / 2)); position.top = fromBounds.top - options.vertSpace - size.height; } else if (direction === 'left') { position.left = fromBounds.left - options.horzSpace - size.width; position.top = Math.floor(fromBounds.centerTop - (size.height / 2)); } else if (direction === 'bottom') { position.left = Math.floor(fromBounds.centerLeft - (size.width / 2)); position.top = fromBounds.bottom + options.vertSpace; } else if (direction === 'right') { position.left = fromBounds.right + options.horzSpace; position.top = Math.floor(fromBounds.centerTop - (size.height / 2)); } if (node.direction) delete node.direction; if (node.directionFrom) delete node.directionFrom; node.customPos = true; } else if (parents.length) { var parentsBounds; var siblingsIndex = 0; $.each(parents, function(idx, parentNode) { if (!parentNode.position) { return; } var parentPosition = parentNode.position; var parentSize = parentNode.size; if (parentsBounds) { parentsBounds.left = Math.min(parentsBounds.left, parentPosition.left); parentsBounds.top = Math.min(parentsBounds.top, parentPosition.top); parentsBounds.right = Math.max(parentsBounds.right, parentSize.width + parentPosition.left); parentsBounds.bottom = Math.max(parentsBounds.bottom, parentSize.height + parentPosition.top); } else { parentsBounds = { left: parentPosition.left, top: parentPosition.top, right: parentSize.width + parentPosition.left, bottom: parentSize.height + parentPosition.top }; } if (node.siblingsIndex === undefined) { var parentChildren = parentNode.children; if (parentChildren.length) { $.each(parentChildren, function(_1, childNode) { if (childNode.siblingsIndex === undefined) { childNode.siblingsIndex = siblingsIndex++; } }); } } }); node.parentsBounds = parentsBounds; position.left = parentsBounds.right + options.horzSpace; position.top = parentsBounds.top + node.siblingsIndex * (options.nodeHeight + options.vertSpace); } else { var bounds = that.bounds; position.left = bounds.left; position.top = bounds.top + bounds.height + (bounds.height <= options.padding ? 0: (options.vertSpace - options.padding)); } node.position = position; if (options.disableAutoPosition) { node.customPos = true; } } if (!skipPosition) { var adsorptionGrid = that.options.adsorptionGrid; if (adsorptionGrid) { position.left = Math.round(position.left / adsorptionGrid) * adsorptionGrid; position.top = Math.round(position.top / adsorptionGrid) * adsorptionGrid; } node.$ele.css({ visibility: 'visible', left: position.left, top: position.top, minWidth: size.width, }); } that.calcNodeBounds(node); }; FlowChart.prototype.calcNodeBounds = function(node) { var bounds = this.bounds; var position = node.position; var size = node.size; var centerLeft = position.left + size.width / 2; var centerTop = position.top + size.height / 2; var centerDistance = Math.sqrt(centerLeft * centerLeft + centerTop * centerTop); bounds.left = Math.min(bounds.left, position.left); bounds.top = Math.min(bounds.top, position.top); bounds.width = Math.max(bounds.width, position.left + size.width); bounds.height = Math.max(bounds.height, position.top + size.height); node.bounds = { left: position.left, top: position.top, width: size.width, height: size.height, right: position.left + size.width, bottom: position.top + size.height, centerLeft: centerLeft, centerTop: centerTop, centerDistance: centerDistance, }; return node.bounds; }; // Render relation FlowChart.prototype.renderRelation = function(relation) { var that = this; var options = that.options; var $relation = $(that._getDomID(relation)); if (!$relation.length && relation.visible) { $relation = $('
').appendTo(that.$canvas); } if (!relation.visible) { $relation.remove(); return; } $relation.addClass(relation.className); relation.$ele = $relation; // Caculate relatin bounds var fromNode = relation.fromNode; var toNode = relation.toNode; var fromNodeBounds = fromNode.bounds; var toNodeBounds = toNode.bounds; // Calculate vert space var vertCenterDistance = fromNodeBounds.centerTop - toNodeBounds.centerTop; var horzCenterDistance = fromNodeBounds.centerLeft - toNodeBounds.centerLeft; var vertDistance = Math.abs(vertCenterDistance) - (fromNodeBounds.height + toNodeBounds.height) / 2; var horzDistance = Math.abs(horzCenterDistance) - (fromNodeBounds.width + toNodeBounds.width) / 2; var direction, isReverse, fromSide, toSide; if (vertDistance >= 0) { if (horzDistance >= 0) { // Four corners if (vertCenterDistance > 0) { // Top if (horzCenterDistance > 0) { // Left direction = 'top-left'; isReverse = true; fromSide = 'left'; toSide = 'bottom'; } else { // Right direction = 'top-right'; isReverse = false; fromSide = 'right'; toSide = 'top'; } } else { // Bottom if (horzCenterDistance > 0) { // Left direction = 'bottom-left'; isReverse = true; fromSide = 'bottom'; toSide = 'right'; } else { // Right direction = 'bottom-right'; isReverse = false; fromSide = 'bottom'; toSide = 'left'; } } } else { isReverse = vertCenterDistance > 0; direction = isReverse ? 'top' : 'bottom'; fromSide = direction; toSide = isReverse ? 'bottom' : 'top'; } } else { isReverse = horzCenterDistance > 0; direction = isReverse ? 'left' : 'right'; fromSide = direction; toSide = isReverse ? 'right' : 'left'; } relation.direction = direction; relation.isReverse = isReverse; relation.fromSide = fromSide; relation.toSide = toSide; var beginNode = isReverse ? toNode : fromNode; var endNode = isReverse ? fromNode : toNode; var beginBounds = beginNode.bounds; var endBounds = endNode.bounds; var beginSide = isReverse ? toSide : fromSide; var endSide = isReverse ? fromSide : toSide; var betterLines = !options.mergeSideLines; var beginSideRels = betterLines && beginNode.sideRels[beginSide]; var endSideRels = betterLines && endNode.sideRels[endSide]; if (betterLines) { var findRelById = function(rel) { return rel.id === relation.id; }; if (!beginSideRels.length || !beginSideRels.find(findRelById)) { beginSideRels.push(relation); if (beginSideRels.length > 1) { beginSideRels.forEach(function(rel) { if (rel.id !== relation.id) { that.renderRelation(rel); } }); } } if (!endSideRels.length || !endSideRels.find(findRelById)) { endSideRels.push(relation); if (endSideRels.length > 1) { endSideRels.forEach(function(rel) { if (rel.id !== relation.id) { that.renderRelation(rel); } }); } } } var beginSideRelIndex = betterLines && beginSideRels.findIndex(findRelById); var endSideRelIndex = betterLines && endSideRels.findIndex(findRelById); var beginSideRelsCount = betterLines ? beginSideRels.length : 0; var endSideRelsCount = betterLines ? endSideRels.length : 0; var bounds = {}; var beginLineStyle = {}; var centerLineStyle = { alignItems: 'center', justifyContent: 'center', }; var endLineStyle = {}; var lineColor = options.relationLineColor; var lineSize = options.relationLineWidth; var hideArrow = options.hideArrowToResult && endNode.type === 'result'; var arrowSize = hideArrow ? 0 : options.relationArrowSize; var arrowStyle = hideArrow ? {display: 'none'} : { display: 'block', position: 'absolute', width: 0, height: 0, borderTopWidth: arrowSize / 2, borderRightWidth: arrowSize / 2, borderBottomWidth: arrowSize / 2, borderLeftWidth: arrowSize / 2, borderTopColor: 'transparent', borderRightColor: 'transparent', borderBottomColor: 'transparent', borderLeftColor: 'transparent', borderStyle: 'solid', }; var baseLineStyle = { background: lineColor, position: 'absolute', }; if (direction === 'bottom-left' || direction === 'top-right') { // Relation link as ┘ bounds.left = beginBounds.right; bounds.top = endBounds.bottom; bounds.width = endBounds.right - bounds.left; bounds.height = beginBounds.bottom - bounds.top; beginLineStyle.left = isReverse ? arrowSize : 0; beginLineStyle.top = Math.floor(beginBounds.centerTop - bounds.top - lineSize / 2); beginLineStyle.width = Math.floor(bounds.width - lineSize - endBounds.width / 2) - beginLineStyle.left; beginLineStyle.height = lineSize; endLineStyle.left = beginLineStyle.width + beginLineStyle.left; endLineStyle.top = isReverse ? 0 : arrowSize; endLineStyle.width = lineSize; endLineStyle.height = beginLineStyle.top - endLineStyle.top; centerLineStyle.left = endLineStyle.left; centerLineStyle.top = beginLineStyle.top; centerLineStyle.height = lineSize; centerLineStyle.width = lineSize; if (options.showRelationTextOnSide) { centerLineStyle.justifyContent = 'flex-start'; centerLineStyle.textIndent = '4px'; } if (!hideArrow) { if (isReverse) { // render left arrow arrowStyle.borderRightWidth = arrowSize; arrowStyle.borderLeftWidth = 0; arrowStyle.borderRightColor = lineColor; arrowStyle.left = 0; arrowStyle.top = Math.floor(beginLineStyle.top + lineSize / 2 - arrowSize / 2); } else { // render top arrow arrowStyle.borderBottomWidth = arrowSize; arrowStyle.borderTopWidth = 0; arrowStyle.borderBottomColor = lineColor; arrowStyle.top = 0; arrowStyle.left = Math.floor(endLineStyle.left + lineSize / 2 - arrowSize / 2); } } } else if (direction === 'top-left' || direction === 'bottom-right') { // Relation link as └ bounds.left = beginBounds.left; bounds.top = beginBounds.bottom; bounds.width = endBounds.left - bounds.left; bounds.height = beginBounds.bottom - bounds.top; beginLineStyle.left = Math.floor(beginBounds.width / 2 - lineSize); beginLineStyle.top = isReverse ? arrowSize : 0; beginLineStyle.width = lineSize; beginLineStyle.height = Math.floor(endBounds.centerTop - bounds.top - lineSize / 2) - beginLineStyle.top; endLineStyle.left = beginLineStyle.left + lineSize; endLineStyle.top = beginLineStyle.top + beginLineStyle.height; endLineStyle.width = Math.floor(bounds.width - lineSize - beginLineStyle.left) - (isReverse ? 0 : arrowSize); endLineStyle.height = lineSize; centerLineStyle.top = endLineStyle.top; centerLineStyle.left = beginLineStyle.left; centerLineStyle.height = lineSize; centerLineStyle.width = lineSize; if (options.showRelationTextOnSide) { centerLineStyle.justifyContent = 'flex-end'; } if (!hideArrow) { if (isReverse) { // render top arrow arrowStyle.borderBottomWidth = arrowSize; arrowStyle.borderTopWidth = 0; arrowStyle.borderBottomColor = lineColor; arrowStyle.top = 0; arrowStyle.left = Math.floor(beginLineStyle.left + lineSize / 2 - arrowSize / 2); } else { // render right arrow arrowStyle.borderLeftWidth = arrowSize; arrowStyle.borderRightWidth = 0; arrowStyle.borderLeftColor = lineColor; arrowStyle.left = endLineStyle.left + endLineStyle.width; arrowStyle.top = Math.floor(endLineStyle.top + lineSize / 2 - arrowSize / 2); } } } else if (direction === 'left' || direction === 'right') { bounds.left = beginBounds.right; bounds.top = Math.min(beginBounds.top, endBounds.top); bounds.width = endBounds.left - bounds.left; bounds.height = Math.max(beginBounds.bottom, endBounds.bottom) - bounds.top; beginLineStyle.left = isReverse ? arrowSize : 0; beginLineStyle.top = Math.floor(beginBounds.centerTop - bounds.top - lineSize / 2); beginLineStyle.width = bounds.width / 2 - beginLineStyle.left; beginLineStyle.height = lineSize; endLineStyle.left = bounds.width / 2; endLineStyle.top = Math.floor(endBounds.centerTop - bounds.top - lineSize / 2); endLineStyle.width = bounds.width / 2 - (isReverse ? 0 : arrowSize); endLineStyle.height = lineSize; centerLineStyle.left = Math.floor(bounds.width / 2 - lineSize / 2); centerLineStyle.top = Math.floor(Math.min(endBounds.centerTop, beginBounds.centerTop) - bounds.top - lineSize / 2); centerLineStyle.height = Math.floor(Math.abs(endBounds.centerTop - beginBounds.centerTop) + lineSize); centerLineStyle.width = lineSize; if (options.showRelationTextOnSide) { if (centerLineStyle.height <= 10) { centerLineStyle.alignItems = 'flex-end'; } else { centerLineStyle.justifyContent = 'flex-start'; centerLineStyle.textIndent = '4px'; } } if (!hideArrow) { if (isReverse) { // render left arrow arrowStyle.borderRightWidth = arrowSize; arrowStyle.borderLeftWidth = 0; arrowStyle.borderRightColor = lineColor; arrowStyle.left = 0; arrowStyle.top = Math.floor(beginLineStyle.top + lineSize / 2 - arrowSize / 2); } else { // render right arrow arrowStyle.borderLeftWidth = arrowSize; arrowStyle.borderRightWidth = 0; arrowStyle.borderLeftColor = lineColor; arrowStyle.left = endLineStyle.left + endLineStyle.width; arrowStyle.top = Math.floor(endLineStyle.top + lineSize / 2 - arrowSize / 2); } } } else if (direction === 'top' || direction === 'bottom') { bounds.left = Math.min(beginBounds.left, endBounds.left); bounds.top = beginBounds.bottom; bounds.width = Math.max(beginBounds.right, endBounds.right) - bounds.left; bounds.height = endBounds.top - bounds.top; beginLineStyle.top = isReverse ? arrowSize : 0; beginLineStyle.left = Math.floor(beginBounds.centerLeft - bounds.left - lineSize / 2); beginLineStyle.height = bounds.height / 2 - beginLineStyle.top; beginLineStyle.width = lineSize; endLineStyle.top = bounds.height / 2; endLineStyle.left = Math.floor(endBounds.centerLeft - bounds.left - lineSize / 2); endLineStyle.height = bounds.height / 2 - (isReverse ? 0 : arrowSize); endLineStyle.width = lineSize; centerLineStyle.top = Math.floor(bounds.height / 2 - lineSize / 2); centerLineStyle.left = Math.floor(Math.min(endBounds.centerLeft, beginBounds.centerLeft) - bounds.left - lineSize / 2); centerLineStyle.width = Math.floor(Math.abs(endBounds.centerLeft - beginBounds.centerLeft) + lineSize); centerLineStyle.height = lineSize; if (options.showRelationTextOnSide) { if (centerLineStyle.width <= 10) { centerLineStyle.justifyContent = 'flex-start'; centerLineStyle.textIndent = '4px'; } else { centerLineStyle.alignItems = 'flex-end'; } } if (!hideArrow) { if (isReverse) { // render top arrow arrowStyle.borderBottomWidth = arrowSize; arrowStyle.borderTopWidth = 0; arrowStyle.borderBottomColor = lineColor; arrowStyle.top = 0; arrowStyle.left = Math.floor(beginLineStyle.left + lineSize / 2 - arrowSize / 2); } else { // render bottom arrow arrowStyle.borderTopWidth = arrowSize; arrowStyle.borderBottomWidth = 0; arrowStyle.borderTopColor = lineColor; arrowStyle.top = endLineStyle.top + endLineStyle.height; arrowStyle.left = Math.floor(endLineStyle.left + lineSize / 2 - arrowSize / 2); } } } $relation.find('.flowchart-rel-arrow').css(arrowStyle); $relation.find('.flowchart-rel-begin-line').css($.extend(beginLineStyle, baseLineStyle)); $relation.find('.flowchart-rel-end-line').css($.extend(endLineStyle, baseLineStyle)); var $centerLine = $relation.find('.flowchart-rel-center-line').css($.extend(centerLineStyle, baseLineStyle)); var relationText = (relation.text === undefined || relation.text === null) ? '' : relation.text; if (options.showRelationTextOnSide) { $centerLine.text(relationText); } else { $centerLine.find('.text').css($.extend({}, options.relationTextStyle)).text(relationText); } $relation.css($.extend({position: 'absolute'}, bounds)); }; FlowChart.prototype._isNodeIntersect = function(node1, node2) { var node1Bounds = node1.bounds; var node2Bounds = node2.bounds; return !((node2Bounds.right < node1Bounds.left) || (node2Bounds.left > node1Bounds.right) || (node2Bounds.bottom < node1Bounds.top) || (node2Bounds.top > node1Bounds.bottom)); }; // Render elements and relations FlowChart.prototype.render = function(partials) { var that = this; var options = that.options; var elements = that.elements; var nodeList = []; var relationList = []; $.each(elements, function(_, element) { if (element.type === 'relation') { relationList.push(element); } else { delete element.siblingsIndex; element.fromRels = []; element.toRels = []; element.children = []; element.parents = []; nodeList.push(element); } }); var elementsSorter = function(e1, e2) { return e1.order - e2.order; }; nodeList.sort(elementsSorter); relationList.sort(elementsSorter); $.each(relationList, function(_, relation) { var fromNode = elements[relation.from]; var toNode = elements[relation.to]; relation.visible = fromNode && toNode; if (relation.visible) { relation.fromIndex = fromNode.fromRels.length; relation.toIndex = fromNode.toRels.length; relation.fromNode = fromNode; relation.toNode = toNode; fromNode.fromRels.push(relation); toNode.toRels.push(relation); fromNode.children.push(toNode); toNode.parents.push(fromNode); } }); var partialsMap = partials ? {} : null; if (partials) { if (!$.isArray(partials)) { partials = [partials]; } $.each(partials, function(_, elementId) { if (typeof elementId === 'object') { elementId = elementId.id; } var element = elements[elementId]; if (element) { partialsMap[elementId] = element; if (element.type !== 'relation') { if (element.fromRels.length) { $.each(element.fromRels, function(_1, rel) { partialsMap[rel.id] = rel; }); } if (element.toRels.length) { $.each(element.toRels, function(_1, rel) { partialsMap[rel.id] = rel; }); } } } }); } else { that.bounds = {left: options.padding, top: options.padding, width: 0, height: 0}; } $.each(nodeList, function(_, node) { if (partialsMap && !partialsMap[node.id]) { return; } that.renderNode(node, true); }); $.each(nodeList, function(_, node) { if (partialsMap && !partialsMap[node.id]) { return; } that._layoutNode(node, !partials); }); // Handle overlay if (!partials) { var needCheckOverlay = true; while(needCheckOverlay) { needCheckOverlay = false; for (var i = nodeList.length - 1; i >= 0; --i) { var nodeA = nodeList[i]; for (var j = nodeList.length - 1; j >= 0; --j) { if (i === j) { continue; } var nodeB = nodeList[j]; if (that._isNodeIntersect(nodeA, nodeB)) { needCheckOverlay = true; nodeA.position.top += options.vertSpace + nodeA.size.height; that.calcNodeBounds(nodeA); } } } }; nodeList.forEach(function(node) { if (partialsMap && !partialsMap[node.id]) { return; } that._layoutNode(node); }); } $.each(relationList, function(_, relation) { if (partialsMap && !partialsMap[relation.id]) { return; } that.renderRelation(relation); }); that.$canvas.css({ minWidth: that.bounds.width + options.padding, minHeight: that.bounds.height + options.padding, }); }; // Set node position FlowChart.prototype.setNodePosition = function(nodeID, position) { var that = this; var node = that.getElement(nodeID); if (!node) { return; } node.position = { left: typeof position.left === 'number' ? position.left : node.position.left, top: typeof position.top === 'number' ? position.top : node.position.top, }; node.customPos = true; that.render(node); }; // Reset all custom positions FlowChart.prototype.resetPosition = function() { $.each(this.elements, function(_, element) { if (element.type !== 'relation' && element.customPos) { delete element.customPos; } }); this.render(); }; // Get element by id FlowChart.prototype.getElement = function(elementID) { if (!elementID) { return null; } if (typeof elementID !== 'string') { elementID = elementID.id; } return this.elements[elementID]; }; // Enter edit mode FlowChart.prototype.enterEditMode = function(ele) { var that = this; ele = that.getElement(ele); if (!ele) { return; } that.exitEditMode(); var $ele = ele.$ele; var $inputBox = ele.isRelation ? $ele.find('.flowchart-relation-text') : $ele; ele.styleBeforeEdit = { zIndex: $inputBox.css('zIndex'), borderColor: $inputBox.css('borderColor'), boxShadow: $inputBox.css('boxShadow'), paddingLeft: $inputBox.css('paddingLeft'), paddingRight: $inputBox.css('paddingRight'), }; $inputBox.css({ zIndex: 10, borderColor: '#3280fc', boxShadow: '0 0 2px #3280fc, 0 0 0 2px rgba(50,128,252,.2)', paddingLeft: 4, paddingRight: 4, }); var $text = ele.isRelation ? $inputBox : $ele.find('.text'); $text.attr('contenteditable', 'true').focus().one('blur.' + that.id, function() { that.exitEditMode(); }); selectText($text); that._currentEditEle = ele; }; // Exit edit mode FlowChart.prototype.exitEditMode = function() { var that = this; var ele = that._currentEditEle; if (!ele) { return; } var $ele = ele.$ele; var $inputBox = ele.isRelation ? $ele.find('.flowchart-relation-text') : $ele; var $text = ele.isRelation ? $inputBox : $ele; $inputBox.css(ele.styleBeforeEdit); $text.attr('contenteditable', ' ').off('.' + that.id); that._currentEditEle = null; that.editNodeText(ele, $text.text()); }; // Edit node text FlowChart.prototype.editNodeText = function(node, text, skipRender) { if (typeof node === 'string') { node = this.elements[node]; } if (!node) { return; } if (node.text !== undefined && node.originText === undefined) { node.originText = node.text; } node.text = text; if (!skipRender) { this.render(node); } }; // Reset data FlowChart.prototype.resetData = function(data) { if (!data) { data = [{ id: 'start', type: 'start', text: 'Start', // className: 'text-red', // style: {color: 'red'} }]; } var that = this; var oldElements = []; $.each(that.elements, function(_, ele) { oldElements.push(ele); }); that.delete(oldElements, true); that.update(data); }; // Update elements data FlowChart.prototype.update = function(elementsData, skipRender) { if (typeof elementsData === 'object' && !$.isArray(elementsData)) { elementsData = [elementsData]; } if (!elementsData || !elementsData.length) return; var that = this; var elements = that.elements; $.each(elementsData, function(_, element) { var newElements = FlowChartElement.create(element); newElements.forEach(function(newElement) { var oldElement = elements[newElement.id]; if (oldElement) { newElement.order = oldElement.order; if (!newElement.position) { newElement.position = oldElement.position; } } elements[newElement.id] = newElement; }); }); if (!skipRender) { that.render(); } }; // Replace element with a new one FlowChart.prototype.replace = function(oldElementID, newElement, skipRender) { if (typeof oldElementID !== 'string') { oldElementID = oldElementID.id; } var that = this; var oldElement = that.getElement(oldElementID); if (oldElement) { that.delete(oldElementID, true); } }; // Get dom id of given element FlowChart.prototype._getDomID = function(element, excludeCssPrefix) { return (excludeCssPrefix ? '' : '#') + this.id + '-' + (typeof element === 'string' ? element : element.id); }; // Delete elements and relations FlowChart.prototype.delete = function(idList, skipRender) { var that = this; if (typeof idList === 'string') { idList = [idList]; } $.each(idList, function(idx, id) { if (typeof id === 'object') { id = id.id; } var $element = that.$canvas.find(that._getDomID(id)); $element.remove(); delete that.elements[id]; }); if (!skipRender) { that.render(); } }; // Export chart data as elements list FlowChart.prototype.exportData = function() { var data = []; $.each(this.elements, function(_, element) { var dataItem = { id: element.id, type: element.type, order: element.order, }; if (element.type === 'relation') { dataItem.from = element.from; dataItem.to = element.to; } else if (element.customPos) { dataItem.position = element.position; } if (element.text !== undefined) { dataItem.text = element.text; } if (element.className !== undefined) { dataItem.className = element.className; } if (element.style !== undefined) { dataItem.style = element.style; } data.push(dataItem); }); data.sort(function(e1, e2) { return e1.order - e2.order; }); $.each(data, function(_, dataItem) { delete dataItem.order; }); return data; }; // Change options FlowChart.prototype.setOptions = function(newOptions, skipRender) { $.extend(this.options, newOptions); if (!skipRender) { this.render(); } }; FlowChart.LANGS = { 'zh-cn': { confirmToDelete: "确定删除【{0}】?", edit: '编辑', 'delete': '删除', 'type': '类型', 'type.action': '动作', 'type.start': '开始', 'type.stop': '结束', 'type.result': '结果', 'type.relation': '关系', }, 'zh-tw': { confirmToDelete: "確定刪除【{0}】?", edit: '編輯', 'delete': '刪除', 'type': '類型', 'type.action': '動作',             'type.start': '開始',             'type.stop': '結束',             'type.result': '結果',             'type.relation': '關係', }, en: { confirmToDelete: "Confirm to delete \"{0}\"?", edit: 'Edit', 'delete': 'Delete', 'type': 'Type', 'type.action': 'Action', 'type.start': 'Start', 'type.stop': 'Stop', 'type.result': 'Result', 'type.relation': 'Relation', }, }, // default options FlowChart.DEFAULTS = { // 是否合并同一方向上的连接线,如果设置为 true,则连接线会尽量保持不重合 // 值为 `true` 的功能暂未实现 mergeSideLines: false, // 是否移动时自动吸附网格 // 如果设置为 true,则设置网格为 5,如果为数值则为指定的网格大小 adsorptionGrid: 5, // 是否启用双击编辑功能 doubleClickToEdit: true, // 是否显示节点上下文菜单(右键菜单) // 此选项可以设置为一个函数动态返回自定义菜单项 showContextMenu: true, // 是否禁用自动位置排列 disableAutoPosition: true, // 是否启用快速添加功能,显示浮动的按钮快捷的向四个方向添加新的节点 quickAdd: true, // 删除节点前是否确认 deleteConfirm: true, // 是否启用拖放移动功能 draggable: true, // 画布宽度,如果设置为 `auto` 则宽度与外部容器元素宽度一致 width: 'auto', // 画布高度 height: 500, // 画布内边距 padding: 20, // 动作节点高度 nodeHeight: 40, // 节点间的水平距离 horzSpace: 80, // 节点间的垂直距离 vertSpace: 60, // 连接线箭头大小 relationArrowSize: 8, // 连接线宽度 relationLineWidth: 1, // 连接线颜色 relationLineColor: '#333', // 隐藏指向结果节点的关系箭头 hideArrowToResult: true, // 节点基本样式 nodeStyle: { }, // 动作节点样式 actionNodeStyle: { borderRadius: '2px', }, // 结果节点样式 resultNodeStyle: { borderColor: 'transparent', padding: '0 3px', minWidth: 40, }, // 开始节点样式 startNodeStyle: { borderRadius: '20px', }, // 停止节点样式 stopNodeStyle: { borderRadius: '20px', }, // 节点上的文本样式 nodeTextStyle: { }, // 关系连接线文本样式 relationTextStyle: { }, // 是否将关系上的文本显示在连接线旁边而不是覆盖在连接线上 showRelationTextOnSide: false, // 初始数据 data: [{ id: 'start', type: 'start', text: 'Start', // className: 'text-red', // style: {color: 'red'} }, /*{ id: 'action-example-1', type: 'action', text: '渲染图形' }, { id: 'action-example-2', type: 'action', text: '显示画面' }, { id: 'action-example-3', type: 'action', text: '额外运算' }, { id: 'action-example-4', type: 'stop', text: '完成' }, { type: 'relation', id: 'relation-1', from: 'start', to: 'action-example-1', text: '工作' }, { type: 'relation', id: 'relation-2', from: 'action-example-1', to: 'action-example-2', }, { type: 'relation', id: 'relation-3', from: 'action-example-2', to: 'action-example-4', text: '休息了' }, { type: 'relation', id: 'relation-4', from: 'action-example-1', to: 'action-example-3', text: '关系', }, { type: 'relation', id: 'relation-5', from: 'action-example-3', to: 'action-example-4', text: 'test', }, { type: 'relation', id: 'relation-6', from: 'action-example-2', to: 'action-example-3', text: 'test2' }*/], }; // Extense jquery element $.fn.flowChart = function(option) { return this.each(function() { var $this = $(this); var data = $this.data(NAME); var options = typeof option == 'object' && option; if(!data) $this.data(NAME, (data = new FlowChart(this, options))); if(typeof option == 'string') data[option](); }); }; FlowChart.NAME = NAME; $.fn.flowChart.Constructor = FlowChart; // Auto call flowChart after document load complete $(function() { $('[data-ride="flowChart"]').flowChart(); }); }(jQuery));