jquery.orgchart.js 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466
  1. /*
  2. * jQuery OrgChart Plugin
  3. * https://github.com/dabeng/OrgChart
  4. *
  5. * Copyright 2016, dabeng
  6. * https://github.com/dabeng
  7. *
  8. * Licensed under the MIT license:
  9. * http://www.opensource.org/licenses/MIT
  10. */
  11. 'use strict';
  12. (function (factory) {
  13. if (typeof module === 'object' && typeof module.exports === 'object') {
  14. factory(require('jquery'), window, document);
  15. } else {
  16. factory(jQuery, window, document);
  17. }
  18. }(function ($, window, document, undefined) {
  19. var OrgChart = function (elem, opts) {
  20. this.$chartContainer = $(elem);
  21. this.opts = opts;
  22. this.defaultOptions = {
  23. 'nodeTitle': 'name',
  24. 'nodeId': 'id',
  25. 'toggleSiblingsResp': false,
  26. 'visibleLevel': 999,
  27. 'chartClass': '',
  28. 'exportButton': false,
  29. 'exportFilename': 'OrgChart',
  30. 'exportFileextension': 'png',
  31. 'parentNodeSymbol': 'fa-users',
  32. 'draggable': false,
  33. 'direction': 't2b',
  34. 'pan': false,
  35. 'zoom': false,
  36. 'zoominLimit': 7,
  37. 'zoomoutLimit': 0.5,
  38. 'exportDataInput':false,
  39. };
  40. };
  41. //
  42. OrgChart.prototype = {
  43. //
  44. init: function (opts) {
  45. var that = this;
  46. this.options = $.extend({}, this.defaultOptions, this.opts, opts);
  47. // build the org-chart
  48. var $chartContainer = this.$chartContainer;
  49. if (this.$chart) {
  50. this.$chart.remove();
  51. }
  52. var data = this.options.data;
  53. var $chart = this.$chart = $('<div>', {
  54. 'data': { 'options': this.options },
  55. 'class': 'orgchart' + (this.options.chartClass !== '' ? ' ' + this.options.chartClass : '') + (this.options.direction !== 't2b' ? ' ' + this.options.direction : ''),
  56. 'click': function(event) {
  57. if (!$(event.target).closest('.node').length) {
  58. $chart.find('.node.focused').removeClass('focused');
  59. }
  60. }
  61. });
  62. if (typeof MutationObserver !== 'undefined') {
  63. this.triggerInitEvent();
  64. }
  65. if ($.type(data) === 'object') {
  66. if (data instanceof $) { // ul datasource
  67. this.buildHierarchy($chart, this.buildJsonDS(data.children()), 0, this.options);
  68. } else { // local json datasource
  69. this.buildHierarchy($chart, this.options.ajaxURL ? data : this.attachRel(data, '00'));
  70. }
  71. } else {
  72. $chart.append('<i class="fa fa-circle-o-notch fa-spin spinner"></i>');
  73. $.ajax({
  74. 'url': data,
  75. 'dataType': 'json'
  76. })
  77. .done(function(data, textStatus, jqXHR) {
  78. that.buildHierarchy($chart, that.options.ajaxURL ? data : that.attachRel(data, '00'), 0, that.options);
  79. })
  80. .fail(function(jqXHR, textStatus, errorThrown) {
  81. console.log(errorThrown);
  82. })
  83. .always(function() {
  84. $chart.children('.spinner').remove();
  85. });
  86. }
  87. $chartContainer.append($chart);
  88. // append the export button
  89. if (this.options.exportButton && !$chartContainer.find('.oc-export-btn').length) {
  90. this.attachExportButton();
  91. }
  92. if (this.options.pan) {
  93. this.bindPan();
  94. }
  95. if (this.options.zoom) {
  96. this.bindZoom();
  97. }
  98. if (this.options.exportDataInput) {
  99. this.exportData();
  100. }
  101. return this;
  102. },
  103. //
  104. triggerInitEvent: function () {
  105. var that = this;
  106. var mo = new MutationObserver(function (mutations) {
  107. mo.disconnect();
  108. initTime:
  109. for (var i = 0; i < mutations.length; i++) {
  110. for (var j = 0; j < mutations[i].addedNodes.length; j++) {
  111. if (mutations[i].addedNodes[j].classList.contains('orgchart')) {
  112. if (that.options.initCompleted && typeof that.options.initCompleted === 'function') {
  113. that.options.initCompleted(that.$chart);
  114. var initEvent = $.Event('init.orgchart');
  115. that.$chart.trigger(initEvent);
  116. break initTime;
  117. }
  118. }
  119. }
  120. }
  121. });
  122. mo.observe(this.$chartContainer[0], { childList: true });
  123. },
  124. //
  125. attachExportButton: function () {
  126. var that = this;
  127. var $exportBtn = $('<button>', {
  128. 'class': 'oc-export-btn' + (this.options.chartClass !== '' ? ' ' + this.options.chartClass : ''),
  129. 'text': 'Export',
  130. 'click': function(e) {
  131. e.preventDefault();
  132. that.export();
  133. }
  134. });
  135. this.$chartContainer.append($exportBtn);
  136. },
  137. setOptions: function (opts, val) {
  138. if (typeof opts === 'string') {
  139. if (opts === 'pan') {
  140. if (val) {
  141. this.bindPan();
  142. } else {
  143. this.unbindPan();
  144. }
  145. }
  146. if (opts === 'zoom') {
  147. if (val) {
  148. this.bindZoom();
  149. } else {
  150. this.unbindZoom();
  151. }
  152. }
  153. }
  154. if (typeof opts === 'object') {
  155. if (opts.data) {
  156. this.init(opts);
  157. } else {
  158. if (typeof opts.pan !== 'undefined') {
  159. if (opts.pan) {
  160. this.bindPan();
  161. } else {
  162. this.unbindPan();
  163. }
  164. }
  165. if (typeof opts.zoom !== 'undefined') {
  166. if (opts.zoom) {
  167. this.bindZoom();
  168. } else {
  169. this.unbindZoom();
  170. }
  171. }
  172. }
  173. }
  174. return this;
  175. },
  176. //
  177. panStartHandler: function (e) {
  178. var $chart = $(e.delegateTarget);
  179. if ($(e.target).closest('.node').length || (e.touches && e.touches.length > 1)) {
  180. $chart.data('panning', false);
  181. return;
  182. } else {
  183. $chart.css('cursor', 'move').data('panning', true);
  184. }
  185. var lastX = 0;
  186. var lastY = 0;
  187. var lastTf = $chart.css('transform');
  188. if (lastTf !== 'none') {
  189. var temp = lastTf.split(',');
  190. if (lastTf.indexOf('3d') === -1) {
  191. lastX = parseInt(temp[4]);
  192. lastY = parseInt(temp[5]);
  193. } else {
  194. lastX = parseInt(temp[12]);
  195. lastY = parseInt(temp[13]);
  196. }
  197. }
  198. var startX = 0;
  199. var startY = 0;
  200. if (!e.targetTouches) { // pand on desktop
  201. startX = e.pageX - lastX;
  202. startY = e.pageY - lastY;
  203. } else if (e.targetTouches.length === 1) { // pan on mobile device
  204. startX = e.targetTouches[0].pageX - lastX;
  205. startY = e.targetTouches[0].pageY - lastY;
  206. } else if (e.targetTouches.length > 1) {
  207. return;
  208. }
  209. $chart.on('mousemove touchmove',function(e) {
  210. if (!$chart.data('panning')) {
  211. return;
  212. }
  213. var newX = 0;
  214. var newY = 0;
  215. if (!e.targetTouches) { // pand on desktop
  216. newX = e.pageX - startX;
  217. newY = e.pageY - startY;
  218. } else if (e.targetTouches.length === 1) { // pan on mobile device
  219. newX = e.targetTouches[0].pageX - startX;
  220. newY = e.targetTouches[0].pageY - startY;
  221. } else if (e.targetTouches.length > 1) {
  222. return;
  223. }
  224. var lastTf = $chart.css('transform');
  225. if (lastTf === 'none') {
  226. if (lastTf.indexOf('3d') === -1) {
  227. $chart.css('transform', 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')');
  228. } else {
  229. $chart.css('transform', 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ' + newX + ', ' + newY + ', 0, 1)');
  230. }
  231. } else {
  232. var matrix = lastTf.split(',');
  233. if (lastTf.indexOf('3d') === -1) {
  234. matrix[4] = ' ' + newX;
  235. matrix[5] = ' ' + newY + ')';
  236. } else {
  237. matrix[12] = ' ' + newX;
  238. matrix[13] = ' ' + newY;
  239. }
  240. $chart.css('transform', matrix.join(','));
  241. }
  242. });
  243. },
  244. //
  245. panEndHandler: function (e) {
  246. if (e.data.chart.data('panning')) {
  247. e.data.chart.data('panning', false).css('cursor', 'default').off('mousemove');
  248. }
  249. },
  250. //
  251. bindPan: function () {
  252. this.$chartContainer.css('overflow', 'hidden');
  253. this.$chart.on('mousedown touchstart', this.panStartHandler);
  254. $(document).on('mouseup touchend', { 'chart': this.$chart }, this.panEndHandler);
  255. },
  256. //
  257. unbindPan: function () {
  258. this.$chartContainer.css('overflow', 'auto');
  259. this.$chart.off('mousedown touchstart', this.panStartHandler);
  260. $(document).off('mouseup touchend', this.panEndHandler);
  261. },
  262. //
  263. zoomWheelHandler: function (e) {
  264. var oc = e.data.oc;
  265. e.preventDefault();
  266. var newScale = 1 + (e.originalEvent.deltaY > 0 ? -0.2 : 0.2);
  267. oc.setChartScale(oc.$chart, newScale);
  268. },
  269. //
  270. zoomStartHandler: function (e) {
  271. if(e.touches && e.touches.length === 2) {
  272. var oc = e.data.oc;
  273. oc.$chart.data('pinching', true);
  274. var dist = oc.getPinchDist(e);
  275. oc.$chart.data('pinchDistStart', dist);
  276. }
  277. },
  278. zoomingHandler: function (e) {
  279. var oc = e.data.oc;
  280. if(oc.$chart.data('pinching')) {
  281. var dist = oc.getPinchDist(e);
  282. oc.$chart.data('pinchDistEnd', dist);
  283. }
  284. },
  285. zoomEndHandler: function (e) {
  286. var oc = e.data.oc;
  287. if(oc.$chart.data('pinching')) {
  288. oc.$chart.data('pinching', false);
  289. var diff = oc.$chart.data('pinchDistEnd') - oc.$chart.data('pinchDistStart');
  290. if (diff > 0) {
  291. oc.setChartScale(oc.$chart, 1.2);
  292. } else if (diff < 0) {
  293. oc.setChartScale(oc.$chart, 0.8);
  294. }
  295. }
  296. },
  297. //
  298. bindZoom: function () {
  299. this.$chartContainer.on('wheel', { 'oc': this }, this.zoomWheelHandler);
  300. this.$chartContainer.on('touchstart', { 'oc': this }, this.zoomStartHandler);
  301. $(document).on('touchmove', { 'oc': this }, this.zoomingHandler);
  302. $(document).on('touchend', { 'oc': this }, this.zoomEndHandler);
  303. },
  304. unbindZoom: function () {
  305. this.$chartContainer.off('wheel', this.zoomWheelHandler);
  306. this.$chartContainer.off('touchstart', this.zoomStartHandler);
  307. $(document).off('touchmove', this.zoomingHandler);
  308. $(document).off('touchend', this.zoomEndHandler);
  309. },
  310. //
  311. getPinchDist: function (e) {
  312. return Math.sqrt((e.touches[0].clientX - e.touches[1].clientX) * (e.touches[0].clientX - e.touches[1].clientX) +
  313. (e.touches[0].clientY - e.touches[1].clientY) * (e.touches[0].clientY - e.touches[1].clientY));
  314. },
  315. //
  316. setChartScale: function ($chart, newScale) {
  317. var opts = $chart.data('options');
  318. var lastTf = $chart.css('transform');
  319. var matrix = '';
  320. var targetScale = 1;
  321. if (lastTf === 'none') {
  322. $chart.css('transform', 'scale(' + newScale + ',' + newScale + ')');
  323. } else {
  324. matrix = lastTf.split(',');
  325. if (lastTf.indexOf('3d') === -1) {
  326. targetScale = Math.abs(window.parseFloat(matrix[3]) * newScale);
  327. if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) {
  328. $chart.css('transform', lastTf + ' scale(' + newScale + ',' + newScale + ')');
  329. }
  330. } else {
  331. targetScale = Math.abs(window.parseFloat(matrix[1]) * newScale);
  332. if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) {
  333. $chart.css('transform', lastTf + ' scale3d(' + newScale + ',' + newScale + ', 1)');
  334. }
  335. }
  336. }
  337. },
  338. //
  339. buildJsonDS: function ($li) {
  340. var that = this;
  341. var subObj = {
  342. 'name': $li.contents().eq(0).text().trim(),
  343. 'relationship': ($li.parent().parent().is('li') ? '1': '0') + ($li.siblings('li').length ? 1: 0) + ($li.children('ul').length ? 1 : 0)
  344. };
  345. $.each($li.data(), function(key, value) {
  346. subObj[key] = value;
  347. });
  348. $li.children('ul').children().each(function() {
  349. if (!subObj.children) { subObj.children = []; }
  350. subObj.children.push(that.buildJsonDS($(this)));
  351. });
  352. return subObj;
  353. },
  354. //
  355. attachRel: function (data, flags) {
  356. var that = this;
  357. data.relationship = flags + (data.children && data.children.length > 0 ? 1 : 0);
  358. if (data.children) {
  359. data.children.forEach(function(item) {
  360. that.attachRel(item, '1' + (data.children.length > 1 ? 1 : 0));
  361. });
  362. }
  363. return data;
  364. },
  365. //
  366. loopChart: function ($chart) {
  367. var that = this;
  368. var $tr = $chart.find('tr:first');
  369. var subObj = { 'id': $tr.find('.node')[0].id };
  370. $tr.siblings(':last').children().each(function() {
  371. if (!subObj.children) { subObj.children = []; }
  372. subObj.children.push(that.loopChart($(this)));
  373. });
  374. return subObj;
  375. },
  376. //
  377. getHierarchy: function () {
  378. if (typeof this.$chart === 'undefined') {
  379. return 'Error: orgchart does not exist'
  380. } else {
  381. if (!this.$chart.find('.node').length) {
  382. return 'Error: nodes do not exist'
  383. } else {
  384. var valid = true;
  385. this.$chart.find('.node').each(function () {
  386. if (!this.id) {
  387. valid = false;
  388. return false;
  389. }
  390. });
  391. if (!valid) {
  392. return 'Error: All nodes of orghcart to be exported must have data-id attribute!';
  393. }
  394. }
  395. }
  396. return this.loopChart(this.$chart);
  397. },
  398. // detect the exist/display state of related node
  399. getNodeState: function ($node, relation) {
  400. var $target = {};
  401. var relation = relation || 'self';
  402. if (relation === 'parent') {
  403. $target = $node.closest('.nodes').siblings(':first');
  404. if ($target.length) {
  405. if ($target.is('.hidden') || (!$target.is('.hidden') && $target.closest('.nodes').is('.hidden'))) {
  406. return { 'exist': true, 'visible': false };
  407. }
  408. return { 'exist': true, 'visible': true };
  409. }
  410. } else if (relation === 'children') {
  411. $target = $node.closest('tr').siblings(':last');
  412. if ($target.length) {
  413. if (!$target.is('.hidden')) {
  414. return { 'exist': true, 'visible': true };
  415. }
  416. return { 'exist': true, 'visible': false };
  417. }
  418. } else if (relation === 'siblings') {
  419. $target = $node.closest('table').parent().siblings();
  420. if ($target.length) {
  421. if (!$target.is('.hidden') && !$target.parent().is('.hidden')) {
  422. return { 'exist': true, 'visible': true };
  423. }
  424. return { 'exist': true, 'visible': false };
  425. }
  426. } else {
  427. $target = $node;
  428. if ($target.length) {
  429. if (!(($target.closest('.nodes').length && $target.closest('.nodes').is('.hidden')) ||
  430. ($target.closest('table').parent().length && $target.closest('table').parent().is('.hidden')) ||
  431. ($target.parent().is('li') && ($target.closest('ul').is('.hidden') || $target.closest('verticalNodes').is('.hidden')))
  432. )) {
  433. return { 'exist': true, 'visible': true };
  434. }
  435. return { 'exist': true, 'visible': false };
  436. }
  437. }
  438. return { 'exist': false, 'visible': false };
  439. },
  440. // find the related nodes
  441. getRelatedNodes: function ($node, relation) {
  442. if (!$node || !($node instanceof $) || !$node.is('.node')) {
  443. return $();
  444. }
  445. if (relation === 'parent') {
  446. return $node.closest('.nodes').parent().children(':first').find('.node');
  447. } else if (relation === 'children') {
  448. return $node.closest('tr').siblings('.nodes').children().find('.node:first');
  449. } else if (relation === 'siblings') {
  450. return $node.closest('table').parent().siblings().find('.node:first');
  451. } else {
  452. return $();
  453. }
  454. },
  455. hideParentEnd: function (event) {
  456. $(event.target).removeClass('sliding');
  457. event.data.upperLevel.addClass('hidden').slice(1).removeAttr('style');
  458. },
  459. // recursively hide the ancestor node and sibling nodes of the specified node
  460. hideParent: function ($node) {
  461. var $upperLevel = $node.closest('.nodes').siblings();
  462. if ($upperLevel.eq(0).find('.spinner').length) {
  463. $node.closest('.orgchart').data('inAjax', false);
  464. }
  465. // hide the sibling nodes
  466. if (this.getNodeState($node, 'siblings').visible) {
  467. this.hideSiblings($node);
  468. }
  469. // hide the lines
  470. var $lines = $upperLevel.slice(1);
  471. $lines.css('visibility', 'hidden');
  472. // hide the superior nodes with transition
  473. var $parent = $upperLevel.eq(0).find('.node');
  474. if (this.getNodeState($parent).visible) {
  475. $parent.addClass('sliding slide-down').one('transitionend', { 'upperLevel': $upperLevel }, this.hideParentEnd);
  476. }
  477. // if the current node has the parent node, hide it recursively
  478. if (this.getNodeState($parent, 'parent').visible) {
  479. this.hideParent($parent);
  480. }
  481. },
  482. showParentEnd: function (event) {
  483. var $node = event.data.node;
  484. $(event.target).removeClass('sliding');
  485. if (this.isInAction($node)) {
  486. this.switchVerticalArrow($node.children('.topEdge'));
  487. }
  488. },
  489. // show the parent node of the specified node
  490. showParent: function ($node) {
  491. // just show only one superior level
  492. var $upperLevel = $node.closest('.nodes').siblings().removeClass('hidden');
  493. // just show only one line
  494. $upperLevel.eq(2).children().slice(1, -1).addClass('hidden');
  495. // show parent node with animation
  496. var $parent = $upperLevel.eq(0).find('.node');
  497. this.repaint($parent[0]);
  498. $parent.addClass('sliding').removeClass('slide-down').one('transitionend', { 'node': $node }, this.showParentEnd.bind(this));
  499. },
  500. stopAjax: function ($nodeLevel) {
  501. if ($nodeLevel.find('.spinner').length) {
  502. $nodeLevel.closest('.orgchart').data('inAjax', false);
  503. }
  504. },
  505. isVisibleNode: function (index, elem) {
  506. return this.getNodeState($(elem)).visible;
  507. },
  508. //
  509. hideChildrenEnd: function (event) {
  510. var $node = event.data.node;
  511. event.data.animatedNodes.removeClass('sliding');
  512. if (event.data.isVerticalDesc) {
  513. event.data.lowerLevel.addClass('hidden');
  514. } else {
  515. event.data.animatedNodes.closest('.nodes').prevAll('.lines').removeAttr('style').addBack().addClass('hidden');
  516. event.data.lowerLevel.last().find('.verticalNodes').addClass('hidden');
  517. }
  518. if (this.isInAction($node)) {
  519. this.switchVerticalArrow($node.children('.bottomEdge'));
  520. }
  521. },
  522. // recursively hide the descendant nodes of the specified node
  523. hideChildren: function ($node) {
  524. var $lowerLevel = $node.closest('tr').siblings();
  525. this.stopAjax($lowerLevel.last());
  526. var $animatedNodes = $lowerLevel.last().find('.node').filter(this.isVisibleNode.bind(this));
  527. var isVerticalDesc = $lowerLevel.last().is('.verticalNodes') ? true : false;
  528. if (!isVerticalDesc) {
  529. $animatedNodes.closest('table').closest('tr').prevAll('.lines').css('visibility', 'hidden');
  530. }
  531. this.repaint($animatedNodes.get(0));
  532. $animatedNodes.addClass('sliding slide-up').eq(0).one('transitionend', { 'animatedNodes': $animatedNodes, 'lowerLevel': $lowerLevel, 'isVerticalDesc': isVerticalDesc, 'node': $node }, this.hideChildrenEnd.bind(this));
  533. },
  534. //
  535. showChildrenEnd: function (event) {
  536. var $node = event.data.node;
  537. event.data.animatedNodes.removeClass('sliding');
  538. if (this.isInAction($node)) {
  539. this.switchVerticalArrow($node.children('.bottomEdge'));
  540. }
  541. },
  542. // show the children nodes of the specified node
  543. showChildren: function ($node) {
  544. var that = this;
  545. var $levels = $node.closest('tr').siblings();
  546. var isVerticalDesc = $levels.is('.verticalNodes') ? true : false;
  547. var $animatedNodes = isVerticalDesc
  548. ? $levels.removeClass('hidden').find('.node').filter(this.isVisibleNode.bind(this))
  549. : $levels.removeClass('hidden').eq(2).children().find('.node:first').filter(this.isVisibleNode.bind(this));
  550. // the two following statements are used to enforce browser to repaint
  551. this.repaint($animatedNodes.get(0));
  552. $animatedNodes.addClass('sliding').removeClass('slide-up').eq(0).one('transitionend', { 'node': $node, 'animatedNodes': $animatedNodes }, this.showChildrenEnd.bind(this));
  553. },
  554. //
  555. hideSiblingsEnd: function (event) {
  556. var $node = event.data.node;
  557. var $nodeContainer = event.data.nodeContainer;
  558. var direction = event.data.direction;
  559. event.data.lines.removeAttr('style');
  560. var $siblings = direction ? (direction === 'left' ? $nodeContainer.prevAll(':not(.hidden)') : $nodeContainer.nextAll(':not(.hidden)')) : $nodeContainer.siblings();
  561. $nodeContainer.closest('.nodes').prev().children(':not(.hidden)')
  562. .slice(1, direction ? $siblings.length * 2 + 1 : -1).addClass('hidden');
  563. event.data.animatedNodes.removeClass('sliding');
  564. $siblings.find('.node:gt(0)').filter(this.isVisibleNode.bind(this))
  565. .removeClass('slide-left slide-right').addClass('slide-up');
  566. $siblings.find('.lines, .nodes, .verticalNodes').addClass('hidden')
  567. .end().addClass('hidden');
  568. if (this.isInAction($node)) {
  569. this.switchHorizontalArrow($node);
  570. }
  571. },
  572. // hide the sibling nodes of the specified node
  573. hideSiblings: function ($node, direction) {
  574. var that = this;
  575. var $nodeContainer = $node.closest('table').parent();
  576. if ($nodeContainer.siblings().find('.spinner').length) {
  577. $node.closest('.orgchart').data('inAjax', false);
  578. }
  579. if (direction) {
  580. if (direction === 'left') {
  581. $nodeContainer.prevAll().find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-right');
  582. } else {
  583. $nodeContainer.nextAll().find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-left');
  584. }
  585. } else {
  586. $nodeContainer.prevAll().find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-right');
  587. $nodeContainer.nextAll().find('.node').filter(this.isVisibleNode.bind(this)).addClass('sliding slide-left');
  588. }
  589. var $animatedNodes = $nodeContainer.siblings().find('.sliding');
  590. var $lines = $animatedNodes.closest('.nodes').prevAll('.lines').css('visibility', 'hidden');
  591. $animatedNodes.eq(0).one('transitionend', { 'node': $node, 'nodeContainer': $nodeContainer, 'direction': direction, 'animatedNodes': $animatedNodes, 'lines': $lines }, this.hideSiblingsEnd.bind(this));
  592. },
  593. //
  594. showSiblingsEnd: function (event) {
  595. var $node = event.data.node;
  596. event.data.visibleNodes.removeClass('sliding');
  597. if (this.isInAction($node)) {
  598. this.switchHorizontalArrow($node);
  599. $node.children('.topEdge').removeClass('fa-chevron-up').addClass('fa-chevron-down');
  600. }
  601. },
  602. //
  603. showRelatedParentEnd: function(event) {
  604. $(event.target).removeClass('sliding');
  605. },
  606. // show the sibling nodes of the specified node
  607. showSiblings: function ($node, direction) {
  608. var that = this;
  609. // firstly, show the sibling td tags
  610. var $siblings = $();
  611. if (direction) {
  612. if (direction === 'left') {
  613. $siblings = $node.closest('table').parent().prevAll().removeClass('hidden');
  614. } else {
  615. $siblings = $node.closest('table').parent().nextAll().removeClass('hidden');
  616. }
  617. } else {
  618. $siblings = $node.closest('table').parent().siblings().removeClass('hidden');
  619. }
  620. // secondly, show the lines
  621. var $upperLevel = $node.closest('table').closest('tr').siblings();
  622. if (direction) {
  623. $upperLevel.eq(2).children('.hidden').slice(0, $siblings.length * 2).removeClass('hidden');
  624. } else {
  625. $upperLevel.eq(2).children('.hidden').removeClass('hidden');
  626. }
  627. // thirdly, do some cleaning stuff
  628. if (!this.getNodeState($node, 'parent').visible) {
  629. $upperLevel.removeClass('hidden');
  630. var parent = $upperLevel.find('.node')[0];
  631. this.repaint(parent);
  632. $(parent).addClass('sliding').removeClass('slide-down').one('transitionend', this.showRelatedParentEnd);
  633. }
  634. // lastly, show the sibling nodes with animation
  635. var $visibleNodes = $siblings.find('.node').filter(this.isVisibleNode.bind(this));
  636. this.repaint($visibleNodes.get(0));
  637. $visibleNodes.addClass('sliding').removeClass('slide-left slide-right');
  638. $visibleNodes.eq(0).one('transitionend', { 'node': $node, 'visibleNodes': $visibleNodes }, this.showSiblingsEnd.bind(this));
  639. },
  640. // start up loading status for requesting new nodes
  641. startLoading: function ($edge) {
  642. var $chart = this.$chart;
  643. if (typeof $chart.data('inAjax') !== 'undefined' && $chart.data('inAjax') === true) {
  644. return false;
  645. }
  646. $edge.addClass('hidden');
  647. $edge.parent().append('<i class="fa fa-circle-o-notch fa-spin spinner"></i>')
  648. .children().not('.spinner').css('opacity', 0.2);
  649. $chart.data('inAjax', true);
  650. $('.oc-export-btn' + (this.options.chartClass !== '' ? '.' + this.options.chartClass : '')).prop('disabled', true);
  651. return true;
  652. },
  653. // terminate loading status for requesting new nodes
  654. endLoading: function ($edge) {
  655. var $node = $edge.parent();
  656. $edge.removeClass('hidden');
  657. $node.find('.spinner').remove();
  658. $node.children().removeAttr('style');
  659. this.$chart.data('inAjax', false);
  660. $('.oc-export-btn' + (this.options.chartClass !== '' ? '.' + this.options.chartClass : '')).prop('disabled', false);
  661. },
  662. // whether the cursor is hovering over the node
  663. isInAction: function ($node) {
  664. return $node.children('.edge').attr('class').indexOf('fa-') > -1 ? true : false;
  665. },
  666. //
  667. switchVerticalArrow: function ($arrow) {
  668. $arrow.toggleClass('fa-chevron-up').toggleClass('fa-chevron-down');
  669. },
  670. //
  671. switchHorizontalArrow: function ($node) {
  672. var opts = this.options;
  673. if (opts.toggleSiblingsResp && (typeof opts.ajaxURL === 'undefined' || $node.closest('.nodes').data('siblingsLoaded'))) {
  674. var $prevSib = $node.closest('table').parent().prev();
  675. if ($prevSib.length) {
  676. if ($prevSib.is('.hidden')) {
  677. $node.children('.leftEdge').addClass('fa-chevron-left').removeClass('fa-chevron-right');
  678. } else {
  679. $node.children('.leftEdge').addClass('fa-chevron-right').removeClass('fa-chevron-left');
  680. }
  681. }
  682. var $nextSib = $node.closest('table').parent().next();
  683. if ($nextSib.length) {
  684. if ($nextSib.is('.hidden')) {
  685. $node.children('.rightEdge').addClass('fa-chevron-right').removeClass('fa-chevron-left');
  686. } else {
  687. $node.children('.rightEdge').addClass('fa-chevron-left').removeClass('fa-chevron-right');
  688. }
  689. }
  690. } else {
  691. var $sibs = $node.closest('table').parent().siblings();
  692. var sibsVisible = $sibs.length ? !$sibs.is('.hidden') : false;
  693. $node.children('.leftEdge').toggleClass('fa-chevron-right', sibsVisible).toggleClass('fa-chevron-left', !sibsVisible);
  694. $node.children('.rightEdge').toggleClass('fa-chevron-left', sibsVisible).toggleClass('fa-chevron-right', !sibsVisible);
  695. }
  696. },
  697. //
  698. repaint: function (node) {
  699. if (node) {
  700. node.style.offsetWidth = node.offsetWidth;
  701. }
  702. },
  703. //
  704. nodeEnterLeaveHandler: function (event) {
  705. var $node = $(event.delegateTarget), flag = false;
  706. var $topEdge = $node.children('.topEdge');
  707. var $rightEdge = $node.children('.rightEdge');
  708. var $bottomEdge = $node.children('.bottomEdge');
  709. var $leftEdge = $node.children('.leftEdge');
  710. if (event.type === 'mouseenter') {
  711. if ($topEdge.length) {
  712. flag = this.getNodeState($node, 'parent').visible;
  713. $topEdge.toggleClass('fa-chevron-up', !flag).toggleClass('fa-chevron-down', flag);
  714. }
  715. if ($bottomEdge.length) {
  716. flag = this.getNodeState($node, 'children').visible;
  717. $bottomEdge.toggleClass('fa-chevron-down', !flag).toggleClass('fa-chevron-up', flag);
  718. }
  719. if ($leftEdge.length) {
  720. this.switchHorizontalArrow($node);
  721. }
  722. } else {
  723. $node.children('.edge').removeClass('fa-chevron-up fa-chevron-down fa-chevron-right fa-chevron-left');
  724. }
  725. },
  726. //
  727. nodeClickHandler: function (event) {
  728. this.$chart.find('.focused').removeClass('focused');
  729. $(event.delegateTarget).addClass('focused');
  730. },
  731. // load new nodes by ajax
  732. loadNodes: function (rel, url, $edge) {
  733. var that = this;
  734. var opts = this.options;
  735. $.ajax({ 'url': url, 'dataType': 'json' })
  736. .done(function (data) {
  737. if (that.$chart.data('inAjax')) {
  738. if (rel === 'parent') {
  739. if (!$.isEmptyObject(data)) {
  740. that.addParent($edge.parent(), data);
  741. }
  742. } else if (rel === 'children') {
  743. if (data.children.length) {
  744. that.addChildren($edge.parent(), data[rel]);
  745. }
  746. } else {
  747. that.addSiblings($edge.parent(), data.siblings ? data.siblings : data);
  748. }
  749. }
  750. })
  751. .fail(function () {
  752. console.log('Failed to get ' + rel + ' data');
  753. })
  754. .always(function () {
  755. that.endLoading($edge);
  756. });
  757. },
  758. //
  759. HideFirstParentEnd: function (event) {
  760. var $topEdge = event.data.topEdge;
  761. var $node = $topEdge.parent();
  762. if (this.isInAction($node)) {
  763. this.switchVerticalArrow($topEdge);
  764. this.switchHorizontalArrow($node);
  765. }
  766. },
  767. //
  768. topEdgeClickHandler: function (event) {
  769. event.stopPropagation();
  770. var that = this;
  771. var $topEdge = $(event.target);
  772. var $node = $(event.delegateTarget);
  773. var parentState = this.getNodeState($node, 'parent');
  774. if (parentState.exist) {
  775. var $parent = $node.closest('table').closest('tr').siblings(':first').find('.node');
  776. if ($parent.is('.sliding')) { return; }
  777. // hide the ancestor nodes and sibling nodes of the specified node
  778. if (parentState.visible) {
  779. this.hideParent($node);
  780. $parent.one('transitionend', { 'topEdge': $topEdge }, this.HideFirstParentEnd.bind(this));
  781. } else { // show the ancestors and siblings
  782. this.showParent($node);
  783. }
  784. } else { // load the new parent node of the specified node by ajax request
  785. // start up loading status
  786. if (this.startLoading($topEdge)) {
  787. var opts = this.options;
  788. var url = $.isFunction(opts.ajaxURL.parent) ? opts.ajaxURL.parent($node.data('nodeData')) : opts.ajaxURL.parent + $node[0].id;
  789. this.loadNodes('parent', url, $topEdge);
  790. }
  791. }
  792. },
  793. //
  794. bottomEdgeClickHandler: function (event) {
  795. event.stopPropagation();
  796. var $bottomEdge = $(event.target);
  797. var $node = $(event.delegateTarget);
  798. var childrenState = this.getNodeState($node, 'children');
  799. if (childrenState.exist) {
  800. var $children = $node.closest('tr').siblings(':last');
  801. if ($children.find('.sliding').length) { return; }
  802. // hide the descendant nodes of the specified node
  803. if (childrenState.visible) {
  804. this.hideChildren($node);
  805. } else { // show the descendants
  806. this.showChildren($node);
  807. }
  808. } else { // load the new children nodes of the specified node by ajax request
  809. if (this.startLoading($bottomEdge)) {
  810. var opts = this.options;
  811. var url = $.isFunction(opts.ajaxURL.children) ? opts.ajaxURL.children($node.data('nodeData')) : opts.ajaxURL.children + $node[0].id;
  812. this.loadNodes('children', url, $bottomEdge);
  813. }
  814. }
  815. },
  816. //
  817. hEdgeClickHandler: function (event) {
  818. event.stopPropagation();
  819. var $hEdge = $(event.target);
  820. var $node = $(event.delegateTarget);
  821. var opts = this.options;
  822. var siblingsState = this.getNodeState($node, 'siblings');
  823. if (siblingsState.exist) {
  824. var $siblings = $node.closest('table').parent().siblings();
  825. if ($siblings.find('.sliding').length) { return; }
  826. if (opts.toggleSiblingsResp) {
  827. var $prevSib = $node.closest('table').parent().prev();
  828. var $nextSib = $node.closest('table').parent().next();
  829. if ($hEdge.is('.leftEdge')) {
  830. if ($prevSib.is('.hidden')) {
  831. this.showSiblings($node, 'left');
  832. } else {
  833. this.hideSiblings($node, 'left');
  834. }
  835. } else {
  836. if ($nextSib.is('.hidden')) {
  837. this.showSiblings($node, 'right');
  838. } else {
  839. this.hideSiblings($node, 'right');
  840. }
  841. }
  842. } else {
  843. if (siblingsState.visible) {
  844. this.hideSiblings($node);
  845. } else {
  846. this.showSiblings($node);
  847. }
  848. }
  849. } else {
  850. // load the new sibling nodes of the specified node by ajax request
  851. if (this.startLoading($hEdge)) {
  852. var nodeId = $node[0].id;
  853. var url = (this.getNodeState($node, 'parent').exist) ?
  854. ($.isFunction(opts.ajaxURL.siblings) ? opts.ajaxURL.siblings($node.data('nodeData')) : opts.ajaxURL.siblings + nodeId) :
  855. ($.isFunction(opts.ajaxURL.families) ? opts.ajaxURL.families($node.data('nodeData')) : opts.ajaxURL.families + nodeId);
  856. this.loadNodes('siblings', url, $hEdge);
  857. }
  858. }
  859. },
  860. //
  861. expandVNodesEnd: function (event) {
  862. event.data.vNodes.removeClass('sliding');
  863. },
  864. //
  865. collapseVNodesEnd: function (event) {
  866. event.data.vNodes.removeClass('sliding').closest('ul').addClass('hidden');
  867. },
  868. // event handler for toggle buttons in Hybrid(horizontal + vertical) OrgChart
  869. toggleVNodes: function (event) {
  870. var $toggleBtn = $(event.target);
  871. var $descWrapper = $toggleBtn.parent().next();
  872. var $descendants = $descWrapper.find('.node');
  873. var $children = $descWrapper.children().children('.node');
  874. if ($children.is('.sliding')) { return; }
  875. $toggleBtn.toggleClass('fa-plus-square fa-minus-square');
  876. if ($descendants.eq(0).is('.slide-up')) {
  877. $descWrapper.removeClass('hidden');
  878. this.repaint($children.get(0));
  879. $children.addClass('sliding').removeClass('slide-up').eq(0).one('transitionend', { 'vNodes': $children }, this.expandVNodesEnd);
  880. } else {
  881. $descendants.addClass('sliding slide-up').eq(0).one('transitionend', { 'vNodes': $descendants }, this.collapseVNodesEnd);
  882. $descendants.find('.toggleBtn').removeClass('fa-minus-square').addClass('fa-plus-square');
  883. }
  884. },
  885. //
  886. createGhostNode: function (event) {
  887. var $nodeDiv = $(event.target);
  888. var opts = this.options;
  889. var origEvent = event.originalEvent;
  890. var isFirefox = /firefox/.test(window.navigator.userAgent.toLowerCase());
  891. var ghostNode, nodeCover;
  892. if (!document.querySelector('.ghost-node')) {
  893. ghostNode = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  894. ghostNode.classList.add('ghost-node');
  895. nodeCover = document.createElementNS('http://www.w3.org/2000/svg','rect');
  896. ghostNode.appendChild(nodeCover);
  897. $nodeDiv.closest('.orgchart').append(ghostNode);
  898. } else {
  899. ghostNode = $nodeDiv.closest('.orgchart').children('.ghost-node').get(0);
  900. nodeCover = $(ghostNode).children().get(0);
  901. }
  902. var transValues = $nodeDiv.closest('.orgchart').css('transform').split(',');
  903. var isHorizontal = opts.direction === 't2b' || opts.direction === 'b2t';
  904. var scale = Math.abs(window.parseFloat(isHorizontal ? transValues[0].slice(transValues[0].indexOf('(') + 1) : transValues[1]));
  905. ghostNode.setAttribute('width', isHorizontal ? $nodeDiv.outerWidth(false) : $nodeDiv.outerHeight(false));
  906. ghostNode.setAttribute('height', isHorizontal ? $nodeDiv.outerHeight(false) : $nodeDiv.outerWidth(false));
  907. nodeCover.setAttribute('x',5 * scale);
  908. nodeCover.setAttribute('y',5 * scale);
  909. nodeCover.setAttribute('width', 120 * scale);
  910. nodeCover.setAttribute('height', 40 * scale);
  911. nodeCover.setAttribute('rx', 4 * scale);
  912. nodeCover.setAttribute('ry', 4 * scale);
  913. nodeCover.setAttribute('stroke-width', 1 * scale);
  914. var xOffset = origEvent.offsetX * scale;
  915. var yOffset = origEvent.offsetY * scale;
  916. if (opts.direction === 'l2r') {
  917. xOffset = origEvent.offsetY * scale;
  918. yOffset = origEvent.offsetX * scale;
  919. } else if (opts.direction === 'r2l') {
  920. xOffset = $nodeDiv.outerWidth(false) - origEvent.offsetY * scale;
  921. yOffset = origEvent.offsetX * scale;
  922. } else if (opts.direction === 'b2t') {
  923. xOffset = $nodeDiv.outerWidth(false) - origEvent.offsetX * scale;
  924. yOffset = $nodeDiv.outerHeight(false) - origEvent.offsetY * scale;
  925. }
  926. if (isFirefox) { // hack for old version of Firefox(< 48.0)
  927. nodeCover.setAttribute('fill', 'rgb(255, 255, 255)');
  928. nodeCover.setAttribute('stroke', 'rgb(191, 0, 0)');
  929. var ghostNodeWrapper = document.createElement('img');
  930. ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode);
  931. origEvent.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset);
  932. } else {
  933. origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset);
  934. }
  935. },
  936. //
  937. filterAllowedDropNodes: function ($dragged) {
  938. var opts = this.options;
  939. var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first');
  940. var $dragHier = $dragged.closest('table').find('.node');
  941. this.$chart.data('dragged', $dragged)
  942. .find('.node').each(function (index, node) {
  943. if ($dragHier.index(node) === -1) {
  944. if (opts.dropCriteria) {
  945. if (opts.dropCriteria($dragged, $dragZone, $(node))) {
  946. $(node).addClass('allowedDrop');
  947. }
  948. } else {
  949. $(node).addClass('allowedDrop');
  950. }
  951. }
  952. });
  953. },
  954. //
  955. dragstartHandler: function (event) {
  956. event.originalEvent.dataTransfer.setData('text/html', 'hack for firefox');
  957. // if users enable zoom or direction options
  958. if (this.$chart.css('transform') !== 'none') {
  959. this.createGhostNode(event);
  960. }
  961. this.filterAllowedDropNodes($(event.target));
  962. },
  963. //
  964. dragoverHandler: function (event) {
  965. event.preventDefault();
  966. if (!$(event.delegateTarget).is('.allowedDrop')) {
  967. event.originalEvent.dataTransfer.dropEffect = 'none';
  968. }
  969. },
  970. //
  971. dragendHandler: function (event) {
  972. this.$chart.find('.allowedDrop').removeClass('allowedDrop');
  973. },
  974. //
  975. dropHandler: function (event) {
  976. var $dropZone = $(event.delegateTarget);
  977. var $dragged = this.$chart.data('dragged');
  978. var $dragZone = $dragged.closest('.nodes').siblings().eq(0).children();
  979. var dropEvent = $.Event('nodedrop.orgchart');
  980. this.$chart.trigger(dropEvent, { 'draggedNode': $dragged, 'dragZone': $dragZone.children(), 'dropZone': $dropZone });
  981. if (dropEvent.isDefaultPrevented()) {
  982. return;
  983. }
  984. // firstly, deal with the hierarchy of drop zone
  985. if (!$dropZone.closest('tr').siblings().length) { // if the drop zone is a leaf node
  986. $dropZone.append('<i class="edge verticalEdge bottomEdge fa"></i>')
  987. .parent().attr('colspan', 2)
  988. .parent().after('<tr class="lines"><td colspan="2"><div class="downLine"></div></td></tr>'
  989. + '<tr class="lines"><td class="rightLine"></td><td class="leftLine"></td></tr>'
  990. + '<tr class="nodes"></tr>')
  991. .siblings(':last').append($dragged.find('.horizontalEdge').remove().end().closest('table').parent());
  992. } else {
  993. var dropColspan = parseInt($dropZone.parent().attr('colspan')) + 2;
  994. var horizontalEdges = '<i class="edge horizontalEdge rightEdge fa"></i><i class="edge horizontalEdge leftEdge fa"></i>';
  995. $dropZone.closest('tr').next().addBack().children().attr('colspan', dropColspan);
  996. if (!$dragged.find('.horizontalEdge').length) {
  997. $dragged.append(horizontalEdges);
  998. }
  999. $dropZone.closest('tr').siblings().eq(1).children(':last').before('<td class="leftLine topLine"></td><td class="rightLine topLine"></td>')
  1000. .end().next().append($dragged.closest('table').parent());
  1001. var $dropSibs = $dragged.closest('table').parent().siblings().find('.node:first');
  1002. if ($dropSibs.length === 1) {
  1003. $dropSibs.append(horizontalEdges);
  1004. }
  1005. }
  1006. // secondly, deal with the hierarchy of dragged node
  1007. var dragColspan = parseInt($dragZone.attr('colspan'));
  1008. if (dragColspan > 2) {
  1009. $dragZone.attr('colspan', dragColspan - 2)
  1010. .parent().next().children().attr('colspan', dragColspan - 2)
  1011. .end().next().children().slice(1, 3).remove();
  1012. var $dragSibs = $dragZone.parent().siblings('.nodes').children().find('.node:first');
  1013. if ($dragSibs.length ===1) {
  1014. $dragSibs.find('.horizontalEdge').remove();
  1015. }
  1016. } else {
  1017. $dragZone.removeAttr('colspan')
  1018. .find('.bottomEdge').remove()
  1019. .end().end().siblings().remove();
  1020. }
  1021. },
  1022. //
  1023. touchstartHandler: function (event) {
  1024. console.log("orgChart: touchstart 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", target=" + event.target.innerText);
  1025. if (this.touchHandled)
  1026. return;
  1027. this.touchHandled = true;
  1028. this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch
  1029. event.preventDefault();
  1030. },
  1031. //
  1032. touchmoveHandler: function (event) {
  1033. if (!this.touchHandled)
  1034. return;
  1035. event.preventDefault();
  1036. if (!this.touchMoved) {
  1037. var nodeIsSelected = $(this).hasClass('focused');
  1038. console.log("orgChart: touchmove 1: " + event.touches.length + " touches, we have not moved, so simulate a drag start", event.touches);
  1039. // TODO: visualise the start of the drag (as would happen on desktop)
  1040. this.simulateMouseEvent(event, 'dragstart');
  1041. }
  1042. this.touchMoved = true;
  1043. var $touching = $(document.elementFromPoint(event.touches[0].clientX, event.touches[0].clientY));
  1044. var $touchingNode = $touching.closest('div.node');
  1045. if ($touchingNode.length > 0) {
  1046. var touchingNodeElement = $touchingNode[0];
  1047. // TODO: simulate the dragover visualisation
  1048. if ($touchingNode.is('.allowedDrop')) {
  1049. console.log("orgChart: touchmove 2: this node (" + touchingNodeElement.id + ") is allowed to be a drop target");
  1050. this.touchTargetNode = touchingNodeElement;
  1051. } else {
  1052. console.log("orgChart: touchmove 3: this node (" + touchingNodeElement.id + ") is NOT allowed to be a drop target");
  1053. this.touchTargetNode = null;
  1054. }
  1055. } else {
  1056. console.log("orgchart: touchmove 4: not touching a node");
  1057. this.touchTargetNode = null;
  1058. }
  1059. },
  1060. //
  1061. touchendHandler: function (event) {
  1062. console.log("orgChart: touchend 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", " + event.target.innerText + " ");
  1063. if (!this.touchHandled) {
  1064. console.log("orgChart: touchend 2: not handled by us, so aborting");
  1065. return;
  1066. }
  1067. if (this.touchMoved) {
  1068. // we've had movement, so this was a 'drag' touch
  1069. if (this.touchTargetNode) {
  1070. console.log("orgChart: touchend 3: moved to a node, so simulating drop");
  1071. var fakeEventForDropHandler = { delegateTarget: this.touchTargetNode };
  1072. this.dropHandler(fakeEventForDropHandler);
  1073. this.touchTargetNode = null;
  1074. }
  1075. console.log("orgChart: touchend 4: simulating dragend");
  1076. this.simulateMouseEvent(event, 'dragend');
  1077. }
  1078. else {
  1079. // we did not move, so assume this was a 'press' touch
  1080. console.log("orgChart: touchend 5: moved, so simulating click");
  1081. this.simulateMouseEvent(event, 'click');
  1082. }
  1083. this.touchHandled = false;
  1084. },
  1085. // simulate a mouse event (so we can fake them on a touch device)
  1086. simulateMouseEvent: function (event, simulatedType) {
  1087. // Ignore multi-touch events
  1088. if (event.originalEvent.touches.length > 1) {
  1089. return;
  1090. }
  1091. var touch = event.originalEvent.changedTouches[0];
  1092. var simulatedEvent = document.createEvent('MouseEvents');
  1093. simulatedEvent.initMouseEvent(
  1094. simulatedType, // type
  1095. true, // bubbles
  1096. true, // cancelable
  1097. window, // view
  1098. 1, // detail
  1099. touch.screenX, // screenX
  1100. touch.screenY, // screenY
  1101. touch.clientX, // clientX
  1102. touch.clientY, // clientY
  1103. false, // ctrlKey
  1104. false, // altKey
  1105. false, // shiftKey
  1106. false, // metaKey
  1107. 0, // button
  1108. null // relatedTarget
  1109. );
  1110. // Dispatch the simulated event to the target element
  1111. event.target.dispatchEvent(simulatedEvent);
  1112. },
  1113. //
  1114. bindDragDrop: function ($node) {
  1115. $node.on('dragstart', this.dragstartHandler.bind(this))
  1116. .on('dragover', this.dragoverHandler.bind(this))
  1117. .on('dragend', this.dragendHandler.bind(this))
  1118. .on('drop', this.dropHandler.bind(this))
  1119. .on('touchstart', this.touchstartHandler.bind(this))
  1120. .on('touchmove', this.touchmoveHandler.bind(this))
  1121. .on('touchend', this.touchendHandler.bind(this));
  1122. },
  1123. // create node
  1124. createNode: function (data) {
  1125. var that = this;
  1126. var opts = this.options;
  1127. var level = data.level;
  1128. if (data.children) {
  1129. $.each(data.children, function (index, child) {
  1130. child.parentId = data.id;
  1131. });
  1132. }
  1133. // construct the content of node
  1134. var $nodeDiv = $('<div' + (opts.draggable ? ' draggable="true"' : '') + (data[opts.nodeId] ? ' id="' + data[opts.nodeId] + '"' : '') + (data.parentId ? ' data-parent="' + data.parentId + '"' : '') + '>')
  1135. .addClass('node ' + (data.className || '') + (level > opts.visibleLevel ? ' slide-up' : ''));
  1136. if (opts.nodeTemplate) {
  1137. $nodeDiv.append(opts.nodeTemplate(data));
  1138. } else {
  1139. $nodeDiv.append('<div class="title">' + data[opts.nodeTitle] + '</div>')
  1140. .append(typeof opts.nodeContent !== 'undefined' ? '<div class="content">' + (data[opts.nodeContent] || '') + '</div>' : '');
  1141. }
  1142. //
  1143. var nodeData = $.extend({}, data);
  1144. delete nodeData.children;
  1145. $nodeDiv.data('nodeData', nodeData);
  1146. // append 4 direction arrows or expand/collapse buttons
  1147. var flags = data.relationship || '';
  1148. if (opts.verticalLevel && level >= opts.verticalLevel) {
  1149. if ((level + 1) > opts.verticalLevel && Number(flags.substr(2,1))) {
  1150. var icon = level + 1 > opts.visibleLevel ? 'plus' : 'minus';
  1151. $nodeDiv.append('<i class="toggleBtn fa fa-' + icon + '-square"></i>');
  1152. }
  1153. } else {
  1154. if (Number(flags.substr(0,1))) {
  1155. $nodeDiv.append('<i class="edge verticalEdge topEdge fa"></i>');
  1156. }
  1157. if(Number(flags.substr(1,1))) {
  1158. $nodeDiv.append('<i class="edge horizontalEdge rightEdge fa"></i>' +
  1159. '<i class="edge horizontalEdge leftEdge fa"></i>');
  1160. }
  1161. if(Number(flags.substr(2,1))) {
  1162. $nodeDiv.append('<i class="edge verticalEdge bottomEdge fa"></i>')
  1163. .children('.title').prepend('<i class="fa '+ opts.parentNodeSymbol + ' symbol"></i>');
  1164. }
  1165. }
  1166. $nodeDiv.on('mouseenter mouseleave', this.nodeEnterLeaveHandler.bind(this));
  1167. $nodeDiv.on('click', this.nodeClickHandler.bind(this));
  1168. $nodeDiv.on('click', '.topEdge', this.topEdgeClickHandler.bind(this));
  1169. $nodeDiv.on('click', '.bottomEdge', this.bottomEdgeClickHandler.bind(this));
  1170. $nodeDiv.on('click', '.leftEdge, .rightEdge', this.hEdgeClickHandler.bind(this));
  1171. $nodeDiv.on('click', '.toggleBtn', this.toggleVNodes.bind(this));
  1172. if (opts.draggable) {
  1173. this.bindDragDrop($nodeDiv);
  1174. this.touchHandled = false;
  1175. this.touchMoved = false;
  1176. this.touchTargetNode = null;
  1177. }
  1178. // allow user to append dom modification after finishing node create of orgchart
  1179. if (opts.createNode) {
  1180. opts.createNode($nodeDiv, data);
  1181. }
  1182. return $nodeDiv;
  1183. },
  1184. // recursively build the tree
  1185. buildHierarchy: function ($appendTo, data) {
  1186. var that = this;
  1187. var opts = this.options;
  1188. var level = 0;
  1189. if (data.level) {
  1190. level = data.level;
  1191. } else {
  1192. level = data.level = $appendTo.parentsUntil('.orgchart', '.nodes').length + 1;
  1193. }
  1194. // Construct the node
  1195. var childrenData = data.children;
  1196. var hasChildren = childrenData ? childrenData.length : false;
  1197. var $nodeWrapper;
  1198. if (Object.keys(data).length > 2) {
  1199. var $nodeDiv = this.createNode(data);
  1200. if (opts.verticalLevel && level >= opts.verticalLevel) {
  1201. $appendTo.append($nodeDiv);
  1202. }else {
  1203. $nodeWrapper = $('<table>');
  1204. $appendTo.append($nodeWrapper.append($('<tr/>').append($('<td' + (hasChildren ? ' colspan="' + childrenData.length * 2 + '"' : '') + '></td>').append($nodeDiv))));
  1205. }
  1206. }
  1207. // Construct the lower level(two "connectiong lines" rows and "inferior nodes" row)
  1208. if (hasChildren) {
  1209. var isHidden = (level + 1 > opts.visibleLevel || data.collapsed) ? ' hidden' : '';
  1210. var isVerticalLayer = (opts.verticalLevel && (level + 1) >= opts.verticalLevel) ? true : false;
  1211. var $nodesLayer;
  1212. if (isVerticalLayer) {
  1213. $nodesLayer = $('<ul>');
  1214. if (isHidden && level + 1 > opts.verticalLevel) {
  1215. $nodesLayer.addClass(isHidden);
  1216. }
  1217. if (level + 1 === opts.verticalLevel) {
  1218. $appendTo.children('table').append('<tr class="verticalNodes' + isHidden + '"><td></td></tr>')
  1219. .find('.verticalNodes').children().append($nodesLayer);
  1220. } else {
  1221. $appendTo.append($nodesLayer);
  1222. }
  1223. } else {
  1224. var $upperLines = $('<tr class="lines' + isHidden + '"><td colspan="' + childrenData.length * 2 + '"><div class="downLine"></div></td></tr>');
  1225. var lowerLines = '<tr class="lines' + isHidden + '"><td class="rightLine"></td>';
  1226. for (var i=1; i<childrenData.length; i++) {
  1227. lowerLines += '<td class="leftLine topLine"></td><td class="rightLine topLine"></td>';
  1228. }
  1229. lowerLines += '<td class="leftLine"></td></tr>';
  1230. $nodesLayer = $('<tr class="nodes' + isHidden + '">');
  1231. if (Object.keys(data).length === 2) {
  1232. $appendTo.append($upperLines).append(lowerLines).append($nodesLayer);
  1233. } else {
  1234. $nodeWrapper.append($upperLines).append(lowerLines).append($nodesLayer);
  1235. }
  1236. }
  1237. // recurse through children nodes
  1238. $.each(childrenData, function () {
  1239. var $nodeCell = isVerticalLayer ? $('<li>') : $('<td colspan="2">');
  1240. $nodesLayer.append($nodeCell);
  1241. this.level = level + 1;
  1242. that.buildHierarchy($nodeCell, this);
  1243. });
  1244. }
  1245. },
  1246. // build the child nodes of specific node
  1247. buildChildNode: function ($appendTo, data) {
  1248. $appendTo.find('td:first').attr('colspan', data.length * 2);
  1249. this.buildHierarchy($appendTo, { 'children': data });
  1250. },
  1251. // exposed method
  1252. addChildren: function ($node, data) {
  1253. this.buildChildNode($node.closest('table'), data);
  1254. if (!$node.children('.bottomEdge').length) {
  1255. $node.append('<i class="edge verticalEdge bottomEdge fa"></i>');
  1256. }
  1257. if (!$node.find('.symbol').length) {
  1258. $node.children('.title').prepend('<i class="fa '+ this.options.parentNodeSymbol + ' symbol"></i>');
  1259. }
  1260. if (this.isInAction($node)) {
  1261. this.switchVerticalArrow($node.children('.bottomEdge'));
  1262. }
  1263. },
  1264. // build the parent node of specific node
  1265. buildParentNode: function ($currentRoot, data) {
  1266. data.relationship = data.relationship || '001';
  1267. var $table = $('<table>')
  1268. .append($('<tr>').append($('<td colspan="2">').append(this.createNode(data))))
  1269. .append('<tr class="lines"><td colspan="2"><div class="downLine"></div></td></tr>')
  1270. .append('<tr class="lines"><td class="rightLine"></td><td class="leftLine"></td></tr>');
  1271. this.$chart.prepend($table)
  1272. .children('table:first').append('<tr class="nodes"><td colspan="2"></td></tr>')
  1273. .children('tr:last').children().append(this.$chart.children('table').last());
  1274. },
  1275. // exposed method
  1276. addParent: function ($currentRoot, data) {
  1277. this.buildParentNode($currentRoot, data);
  1278. if (!$currentRoot.children('.topEdge').length) {
  1279. $currentRoot.children('.title').after('<i class="edge verticalEdge topEdge fa"></i>');
  1280. }
  1281. if (this.isInAction($currentRoot)) {
  1282. this.switchVerticalArrow($currentRoot.children('.topEdge'));
  1283. }
  1284. },
  1285. // subsequent processing of build sibling nodes
  1286. complementLine: function ($oneSibling, siblingCount, existingSibligCount) {
  1287. var lines = '';
  1288. for (var i = 0; i < existingSibligCount; i++) {
  1289. lines += '<td class="leftLine topLine"></td><td class="rightLine topLine"></td>';
  1290. }
  1291. $oneSibling.parent().prevAll('tr:gt(0)').children().attr('colspan', siblingCount * 2)
  1292. .end().next().children(':first').after(lines);
  1293. },
  1294. // build the sibling nodes of specific node
  1295. buildSiblingNode: function ($nodeChart, data) {
  1296. var newSiblingCount = $.isArray(data) ? data.length : data.children.length;
  1297. var existingSibligCount = $nodeChart.parent().is('td') ? $nodeChart.closest('tr').children().length : 1;
  1298. var siblingCount = existingSibligCount + newSiblingCount;
  1299. var insertPostion = (siblingCount > 1) ? Math.floor(siblingCount/2 - 1) : 0;
  1300. // just build the sibling nodes for the specific node
  1301. if ($nodeChart.parent().is('td')) {
  1302. var $parent = $nodeChart.closest('tr').prevAll('tr:last');
  1303. $nodeChart.closest('tr').prevAll('tr:lt(2)').remove();
  1304. this.buildChildNode($nodeChart.parent().closest('table'), data);
  1305. var $siblingTds = $nodeChart.parent().closest('table').children('tr:last').children('td');
  1306. if (existingSibligCount > 1) {
  1307. this.complementLine($siblingTds.eq(0).before($nodeChart.closest('td').siblings().addBack().unwrap()), siblingCount, existingSibligCount);
  1308. } else {
  1309. this.complementLine($siblingTds.eq(insertPostion).after($nodeChart.closest('td').unwrap()), siblingCount, 1);
  1310. }
  1311. } else { // build the sibling nodes and parent node for the specific ndoe
  1312. this.buildHierarchy($nodeChart.closest('.orgchart'), data);
  1313. this.complementLine($nodeChart.next().children('tr:last').children().eq(insertPostion).after($('<td colspan="2">').append($nodeChart)),
  1314. siblingCount, 1);
  1315. }
  1316. },
  1317. //
  1318. addSiblings: function ($node, data) {
  1319. this.buildSiblingNode($node.closest('table'), data);
  1320. $node.closest('.nodes').data('siblingsLoaded', true);
  1321. if (!$node.children('.leftEdge').length) {
  1322. $node.children('.topEdge').after('<i class="edge horizontalEdge rightEdge fa"></i><i class="edge horizontalEdge leftEdge fa"></i>');
  1323. }
  1324. if (this.isInAction($node)) {
  1325. this.switchHorizontalArrow($node);
  1326. $node.children('.topEdge').removeClass('fa-chevron-up').addClass('fa-chevron-down');
  1327. }
  1328. },
  1329. //
  1330. removeNodes: function ($node) {
  1331. var isVerticalNode = $node.parents('.verticalNodes').length > 0 ? true : false
  1332. var $parent = isVerticalNode ? $node.parent() : $node.closest('table').parent();
  1333. var $sibs = isVerticalNode ? $parent.siblings() : $parent.parent().siblings();
  1334. if ($parent.is('td') || $parent.is('li')) {
  1335. if (this.getNodeState($node, 'siblings').exist) {
  1336. $sibs.eq(2).children('.topLine:lt(2)').remove();
  1337. $sibs.slice(0, 2).children().attr('colspan', $sibs.eq(2).children().length);
  1338. $parent.remove();
  1339. } else {
  1340. $sibs.eq(0).children().removeAttr('colspan')
  1341. .find('.bottomEdge').remove()
  1342. .end().end().siblings().remove();
  1343. }
  1344. } else {
  1345. $parent.add($parent.siblings()).remove();
  1346. }
  1347. },
  1348. exportData: function(input) {
  1349. var that = this;
  1350. var $chartContainer = this.$chartContainer;
  1351. //exportFilename = (typeof exportFilename !== 'undefined') ? exportFilename : this.options.exportFilename;
  1352. input = input || this.options.exportDataInput;
  1353. if ($(this).children('.spinner').length) {
  1354. return false;
  1355. }
  1356. //var $mask = $chartContainer.find('.mask');
  1357. //if (!$mask.length) {
  1358. // $chartContainer.append('<div class="mask"><i class="fa fa-circle-o-notch fa-spin spinner"></i></div>');
  1359. //} else {
  1360. // $mask.removeClass('hidden');
  1361. //}
  1362. var img;
  1363. var sourceChart = $chartContainer.addClass('canvasContainer').find('.orgchart:not(".hidden")').get(0);
  1364. var flag = that.options.direction === 'l2r' || that.options.direction === 'r2l';
  1365. html2canvas(sourceChart, {
  1366. 'width': flag ? sourceChart.clientHeight : sourceChart.clientWidth,
  1367. 'height': flag ? sourceChart.clientWidth : sourceChart.clientHeight,
  1368. 'onclone': function (cloneDoc) {
  1369. $(cloneDoc).find('.canvasContainer').css('overflow', 'visible')
  1370. .find('.orgchart:not(".hidden"):first').css('transform', '');
  1371. },
  1372. 'onrendered': function (canvas) {
  1373. //$chartContainer.find('.mask').addClass('hidden');
  1374. img = canvas.toDataURL("image/png");
  1375. //console.log("exportData-", img);
  1376. console.log("exportData-over");
  1377. input.val(img);
  1378. }
  1379. })
  1380. .then(function () {
  1381. $chartContainer.removeClass('canvasContainer');
  1382. }, function () {
  1383. $chartContainer.removeClass('canvasContainer');
  1384. });
  1385. return img;
  1386. },
  1387. //
  1388. export: function (exportFilename, exportFileextension) {
  1389. var that = this;
  1390. exportFilename = (typeof exportFilename !== 'undefined') ? exportFilename : this.options.exportFilename;
  1391. exportFileextension = (typeof exportFileextension !== 'undefined') ? exportFileextension : this.options.exportFileextension;
  1392. if ($(this).children('.spinner').length) {
  1393. return false;
  1394. }
  1395. var $chartContainer = this.$chartContainer;
  1396. var $mask = $chartContainer.find('.mask');
  1397. if (!$mask.length) {
  1398. $chartContainer.append('<div class="mask"><i class="fa fa-circle-o-notch fa-spin spinner"></i></div>');
  1399. } else {
  1400. $mask.removeClass('hidden');
  1401. }
  1402. var sourceChart = $chartContainer.addClass('canvasContainer').find('.orgchart:not(".hidden")').get(0);
  1403. var flag = that.options.direction === 'l2r' || that.options.direction === 'r2l';
  1404. html2canvas(sourceChart, {
  1405. 'width': flag ? sourceChart.clientHeight : sourceChart.clientWidth,
  1406. 'height': flag ? sourceChart.clientWidth : sourceChart.clientHeight,
  1407. 'onclone': function (cloneDoc) {
  1408. $(cloneDoc).find('.canvasContainer').css('overflow', 'visible')
  1409. .find('.orgchart:not(".hidden"):first').css('transform', '');
  1410. },
  1411. 'onrendered': function (canvas) {
  1412. $chartContainer.find('.mask').addClass('hidden');
  1413. if (exportFileextension.toLowerCase() === 'pdf') {
  1414. var doc = {};
  1415. var docWidth = Math.floor(canvas.width * 0.2646);
  1416. var docHeight = Math.floor(canvas.height * 0.2646);
  1417. if (docWidth > docHeight) {
  1418. doc = new jsPDF('l', 'mm', [docWidth, docHeight]);
  1419. } else {
  1420. doc = new jsPDF('p', 'mm', [docHeight, docWidth]);
  1421. }
  1422. doc.addImage(canvas.toDataURL(), 'png', 0, 0);
  1423. doc.save(exportFilename + '.pdf');
  1424. } else {
  1425. var isWebkit = 'WebkitAppearance' in document.documentElement.style;
  1426. var isFf = !!window.sidebar;
  1427. var isEdge = navigator.appName === 'Microsoft Internet Explorer' || (navigator.appName === "Netscape" && navigator.appVersion.indexOf('Edge') > -1);
  1428. if ((!isWebkit && !isFf) || isEdge) {
  1429. window.navigator.msSaveBlob(canvas.msToBlob(), exportFilename + '.png');
  1430. } else {
  1431. var selector = '.oc-download-btn' + (that.options.chartClass !== '' ? '.' + that.options.chartClass : '');
  1432. if (!$chartContainer.find(selector).length) {
  1433. $chartContainer.append('<a class="oc-download-btn' + (that.options.chartClass !== '' ? ' ' + that.options.chartClass : '') + '"'
  1434. + ' download="' + exportFilename + '.png"></a>');
  1435. }
  1436. $chartContainer.find(selector).attr('href', canvas.toDataURL())[0].click();
  1437. }
  1438. }
  1439. }
  1440. })
  1441. .then(function () {
  1442. $chartContainer.removeClass('canvasContainer');
  1443. }, function () {
  1444. $chartContainer.removeClass('canvasContainer');
  1445. });
  1446. return true;
  1447. }
  1448. };
  1449. $.fn.orgchart = function (opts) {
  1450. return new OrgChart(this, opts).init();
  1451. };
  1452. }));