flowtree.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. ;(function (factory) {
  2. if (typeof window.module === 'object' && typeof window.module.exports === 'object') {
  3. factory(window.require('jquery'), window, document);
  4. } else {
  5. factory(jQuery, window, document);
  6. }
  7. }(function ($, window, document, undefined) {
  8. var treeClass = 'iwb-flow-tree',
  9. floorClass = 'floor-box',
  10. nodeFloorClass = 'floor-node',
  11. nodeClass = 'node-box',
  12. lineBoxClass = 'line-box',
  13. lineGroupClass = 'line-group',
  14. lineClass = 'line',
  15. lineSingleClass = 'line-single',
  16. lineUpClass = 'line-u',
  17. lineMidClass = 'line-m',
  18. lineDownClass = 'line-d',
  19. lineDownLeftClass = 'line-d-l',
  20. lineDownRightClass = 'line-d-r';
  21. var allChildrenCount = 0;
  22. var FlowTree = function (elem, opts) {
  23. this.$nodeContainer = $(elem);
  24. this.opts = opts;
  25. this.defaultOptions = {
  26. 'data': {},
  27. 'nodeTitle': 'name',
  28. 'nodeMinWith': 250,
  29. 'nodeId': 'id',
  30. 'parentId': 'parentId',
  31. 'path': 'path',
  32. 'parentPath': 'parentPath',
  33. 'nodeClass': '',
  34. 'visibleLevel': 999,
  35. 'exportButton': false,
  36. 'exportFilename': 'FlowTree',
  37. 'exportFileExtension': 'png',
  38. 'draggable': true,
  39. 'dragDelay':200,
  40. 'zoom': true,
  41. 'zoomMaxLimit': 2,
  42. 'zoomMinLimit': 0.5,
  43. 'exportDataInput': false,
  44. 'customMenu': 'flow-menu',
  45. 'customMenuBefore':null,
  46. 'offset': undefined
  47. };
  48. };
  49. FlowTree.prototype = {
  50. init: function(opts) {
  51. var that = this;
  52. allChildrenCount = 0;
  53. this.options = $.extend({}, this.defaultOptions, this.opts, opts || {});
  54. //
  55. var $nodeContainer = this.$nodeContainer;
  56. that.$nodeTree = $nodeContainer.find('.' + treeClass);
  57. if (this.$nodeTree && this.$nodeTree.length>0) {
  58. if (this.options.offset) {
  59. this.options.offset = this.$nodeTree.offset();
  60. }
  61. this.$nodeTree.remove();
  62. } else {
  63. this.options.offset = false;
  64. }
  65. var data = this.options.data;
  66. var $nodeTree = this.$nodeTree = $('<div>',
  67. {
  68. 'data': { 'options': this.options },
  69. 'class': treeClass + (this.options.nodeClass !== '' ? ' ' + this.options.nodeClass : '')
  70. });
  71. this.buildHierarchy($nodeTree, data);
  72. $nodeContainer.append($nodeTree);
  73. var width = allChildrenCount * this.options.nodeMinWith;
  74. if (width < this.options.nodeMinWith) {
  75. width = this.options.nodeMinWith;
  76. }
  77. $nodeTree.css('width', width + 'px');
  78. if (this.options.offset) {
  79. $nodeTree.offset(this.options.offset);
  80. }
  81. if (this.options.draggable) {
  82. this.drag(this.$nodeTree);
  83. }
  84. if (this.options.zoom) {
  85. this.$nodeContainer.on('wheel', { 'nt': that }, this.zoomWheel);
  86. }
  87. return this;
  88. },
  89. buildHierarchy: function($appendTo, data) {
  90. var that = this;
  91. var $floorBox = $('<div>', { 'class': floorClass });
  92. if (!data) {
  93. $floorBox.append('暂无数据!');
  94. return;
  95. }
  96. that.buildChildNode($floorBox, data);
  97. $appendTo.append($floorBox);
  98. },
  99. buildChildNode: function ($appendTo, data) {
  100. var that = this;
  101. //var opts = this.options;
  102. var childrenData = data.children;
  103. var hasChildren = childrenData ? childrenData.length : false;
  104. var $nodeDiv = that.createNode(data);
  105. var $floorNode = $('<div>', { 'class': nodeFloorClass });
  106. $floorNode.append($nodeDiv);
  107. if (hasChildren) {
  108. that.buildLine($floorNode,hasChildren);
  109. var $floorChildBox = $('<div>', { 'class': floorClass });
  110. childrenData.forEach(function(v) {
  111. allChildrenCount++;
  112. that.buildHierarchy($floorChildBox, v);
  113. });
  114. $floorNode.append($floorChildBox);
  115. }
  116. $appendTo.append($floorNode);
  117. },
  118. buildLine: function ($appendTo,childrenCount) {
  119. var $lineBox = $('<div>', { 'class': lineBoxClass });
  120. var $lineGroupUp = $('<div>', { 'class': lineGroupClass });
  121. if (childrenCount === 1) {
  122. $lineGroupUp.append('<div class="' +
  123. lineSingleClass +
  124. ' ' + lineClass + '" style="width:100%"><div class="' +
  125. lineDownClass +
  126. '"></div></div>');
  127. $lineBox.append($lineGroupUp);
  128. }
  129. else {
  130. $lineGroupUp.append('<div class="' +
  131. lineClass +
  132. '" style="width: 100%"><div class="' +
  133. lineUpClass +
  134. '"></div></div>');
  135. var $lineGroupMid = $('<div>', { 'class': lineGroupClass });
  136. var hiddenWidth = 50.00 / childrenCount;
  137. $lineGroupMid.append(
  138. '<div class="' +
  139. lineMidClass +
  140. '" style="width:' +
  141. hiddenWidth +
  142. '%;visibility: hidden; "></div><div class= "' +
  143. lineMidClass +
  144. '" style = "width: ' +
  145. (100 - hiddenWidth * 2) +
  146. '%;" ></div><div class="' +
  147. lineMidClass +
  148. '" style="width: ' +
  149. hiddenWidth +
  150. '%;visibility: hidden;"></div>');
  151. var $lineGroupDown = $('<div>', { 'class': lineGroupClass });
  152. var width = 100.00 / childrenCount;
  153. $lineGroupDown.append('<div class="' +
  154. lineClass +
  155. '" style="width: ' +
  156. width +
  157. '%"><div class="' +
  158. lineDownClass +
  159. ' ' +
  160. lineDownLeftClass +
  161. '"></div></div>');
  162. for (var j = 0; j < childrenCount - 2; j++) {
  163. $lineGroupDown.append('<div class="' +
  164. lineClass +
  165. '" style="width: ' +
  166. width +
  167. '%"><div class="' +
  168. lineDownClass +
  169. '"></div></div>');
  170. }
  171. $lineGroupDown.append('<div class="' +
  172. lineClass +
  173. '" style="width: ' +
  174. width +
  175. '%"><div class="' +
  176. lineDownClass +
  177. ' ' +
  178. lineDownRightClass +
  179. '"></div></div>');
  180. $lineBox.append($lineGroupUp).append($lineGroupMid).append($lineGroupDown);
  181. }
  182. $appendTo.append($lineBox);
  183. },
  184. createNode: function (data) {
  185. var that = this;
  186. var opts = that.options;
  187. var $nodeDiv =
  188. $('<div' +
  189. (data[opts.nodeId] ? ' id="' + data[opts.nodeId] + '"' : '') +
  190. (data[opts.parentId] ? ' data-parent="' + data[opts.parentId] + '"' : '') +
  191. '>').addClass(nodeClass + ' ' + (data.className || ''));
  192. if (opts.nodeTemplate) {
  193. $nodeDiv.append(opts.nodeTemplate(data));
  194. } else {
  195. $nodeDiv.append('<div class="title">' + data[opts.nodeTitle] + '</div>')
  196. .append(typeof opts.nodeContent !== 'undefined'
  197. ? '<div class="content">' + (data[opts.nodeContent] || '') + '</div>'
  198. : '');
  199. }
  200. $nodeDiv.find('[data-toggle="tip"]').tooltip({ 'placement': 'right', 'container': 'body', 'delay': 500 });
  201. //
  202. var nodeData = $.extend({}, data);
  203. delete nodeData.children;
  204. $nodeDiv.data('nodeData', nodeData);
  205. $nodeDiv.data('node-id', data[opts.nodeId]);
  206. $nodeDiv.data('path', data[opts.path]);
  207. $nodeDiv.data('parent-path', data[opts.parentPath]);
  208. $nodeDiv.find('.title').data('path', data[opts.path]);
  209. $nodeDiv.find('.title').data('parent-path', data[opts.parentPath]);
  210. $nodeDiv.find('.title-text').data('path', data[opts.path]);
  211. $nodeDiv.find('.title-text').data('parent-path', data[opts.parentPath]);
  212. // allow user to append dom modification after finishing node create of node tree
  213. if (opts.createNode) {
  214. opts.createNode($nodeDiv, data);
  215. }
  216. if (opts.customMenu) {
  217. this.customMenu($nodeDiv.find('.title'));
  218. this.customMenu($nodeDiv.find('.title-text'));
  219. }
  220. return $nodeDiv;
  221. },
  222. customMenu: function ($node) {
  223. var that = this;
  224. var opts = that.options;
  225. var $menu = typeof opts.customMenu === 'string' ? $('#' + opts.customMenu) : $(opts.customMenu);;
  226. $node.css('cursor', 'help');
  227. $node.contextmenu(function (e) {
  228. if (opts.customMenuBefore && $.type(opts.customMenuBefore) === 'function') {
  229. opts.customMenuBefore($node);
  230. }
  231. e = e || window.event;
  232. //鼠标点的坐标
  233. var oX = e.clientX;
  234. var oY = e.clientY;
  235. //菜单出现后的位置
  236. $menu.fadeIn();
  237. $menu.offset({ top: oY, left: oX });
  238. //阻止浏览器默认事件
  239. return false; //一般点击右键会出现浏览器默认的右键菜单,写了这句代码就可以阻止该默认事件。)
  240. });
  241. $(document).on('click', function () {
  242. //e = e || window.event;
  243. $menu.hide();
  244. });
  245. $node.on('click', function (e) {
  246. e = e || window.event;
  247. e.cancelBubble = true;
  248. });
  249. },
  250. zoomWheel: function(e) {
  251. var nt = e.data.nt;
  252. e.preventDefault();
  253. var newScale = 1 + (e.originalEvent.deltaY > 0 ? -0.05 : 0.05);
  254. nt.setChartScale(nt.$nodeTree, newScale);
  255. },
  256. setChartScale: function ($nodeTree, newScale) {
  257. var that = this;
  258. var opts = that.options;
  259. var lastTf = $nodeTree.css('transform');
  260. var matrix = '';
  261. var targetScale = 1;
  262. if (lastTf === 'none') {
  263. $nodeTree.css('transform', 'scale(' + newScale + ',' + newScale + ')');
  264. } else {
  265. matrix = lastTf.split(',');
  266. if (lastTf.indexOf('3d') === -1) {
  267. targetScale = Math.abs(window.parseFloat(matrix[3]) * newScale);
  268. if (targetScale > opts.zoomMinLimit && targetScale < opts.zoomMaxLimit) {
  269. $nodeTree.css('transform', lastTf + ' scale(' + newScale + ',' + newScale + ')');
  270. }
  271. } else {
  272. targetScale = Math.abs(window.parseFloat(matrix[1]) * newScale);
  273. if (targetScale > opts.zoomMinLimit && targetScale < opts.zoomMaxLimit) {
  274. $nodeTree.css('transform', lastTf + ' scale3d(' + newScale + ',' + newScale + ', 1)');
  275. }
  276. }
  277. }
  278. },
  279. drag: function($node) {
  280. var that = this;
  281. var opts = that.options;
  282. var x = 0;
  283. var y = 0;
  284. var l = 0;
  285. var t = 0;
  286. var cursor ='pointer';
  287. var timeout = undefined;
  288. $node.data('drag', false);
  289. //点击(松开后触发)
  290. $node.mousedown(function (e) {
  291. if (e.which === 3) {//禁止右键拖动
  292. return;
  293. }
  294. var $this = $(this);
  295. timeout = setTimeout(function () {
  296. //获取x坐标和y坐标
  297. x = e.clientX;
  298. y = e.clientY;
  299. //获取左部和顶部的偏移量
  300. l = that.$nodeTree.offset().left;
  301. t = that.$nodeTree.offset().top;
  302. //开关打开
  303. $node.data('drag', true);
  304. that.$nodeTree.fadeTo(20, 0.5);
  305. //设置样式
  306. cursor = $this.css('cursor');
  307. $this.css('cursor', 'move');
  308. }, opts.dragDelay);
  309. });
  310. that.$nodeContainer.mousemove(function(e) {
  311. e.preventDefault();
  312. if ($node.data('drag') !== true) {
  313. return;
  314. }
  315. //获取x和y
  316. var nx = e.clientX;
  317. var ny = e.clientY;
  318. //计算移动后的左偏移量和顶部的偏移量
  319. var nl = nx - (x - l);
  320. var nt = ny - (y - t);
  321. that.$nodeTree.offset({ top: nt, left: nl });
  322. });
  323. $node.mouseup(function() {
  324. //开关打开
  325. $node.data('drag', false);
  326. that.$nodeTree.fadeTo("fast", 1);
  327. //设置样式
  328. $(this).css('cursor', cursor);
  329. clearTimeout(timeout);
  330. });
  331. },
  332. exportData: function (input) {
  333. var that = this;
  334. var $nodeContainer = this.$nodeContainer;
  335. input = input || this.options.exportDataInput;
  336. if ($(this).children('.spinner').length) {
  337. return false;
  338. }
  339. var img;
  340. var sourceChart = $nodeContainer.addClass('canvasContainer').find('.'+treeClass+':not(".hidden")').get(0);
  341. var flag = that.options.direction === 'l2r' || that.options.direction === 'r2l';
  342. window.html2canvas(sourceChart, {
  343. 'width': flag ? sourceChart.clientHeight : sourceChart.clientWidth,
  344. 'height': flag ? sourceChart.clientWidth : sourceChart.clientHeight,
  345. 'onclone': function (cloneDoc) {
  346. $(cloneDoc).find('.canvasContainer').css('overflow', 'visible')
  347. .find('.' + treeClass +':not(".hidden"):first').css('transform', '');
  348. },
  349. 'onrendered': function (canvas) {
  350. img = canvas.toDataURL("image/png");
  351. console.log("exportData-over");
  352. input.val(img);
  353. }
  354. })
  355. .then(function () {
  356. $nodeContainer.removeClass('canvasContainer');
  357. });
  358. return img;
  359. }
  360. };
  361. $.fn.flowTree = function (opts) {
  362. return new FlowTree(this, opts).init();
  363. };
  364. }));