orgchart 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.github/workflows/publish.yml +32 -0
  2. package/README.md +172 -93
  3. package/README.zh-cn.md +163 -74
  4. package/README.zh-tw.md +163 -74
  5. package/bower.json +6 -9
  6. package/demo/10000-nodes.html +139 -0
  7. package/demo/ajax-datasource.html +46 -45
  8. package/demo/bottom2top.html +12 -9
  9. package/demo/center-on-selected-node.html +21 -17
  10. package/demo/color-coded.html +12 -9
  11. package/demo/css/style.css +28 -0
  12. package/demo/custom-icons.html +12 -9
  13. package/demo/custom-template.html +12 -9
  14. package/demo/data-prop-compact.html +11 -8
  15. package/demo/data-prop-hybrid.html +79 -50
  16. package/demo/default-collapsed.html +12 -9
  17. package/demo/drag-drop-hybrid-chart.html +17 -14
  18. package/demo/drag-drop.html +21 -16
  19. package/demo/edit-chart.html +148 -94
  20. package/demo/edit-hybrid-chart.html +178 -135
  21. package/demo/expand-to-given-depth.html +30 -22
  22. package/demo/export-chart-pdf.html +12 -9
  23. package/demo/export-chart-with-pictures.html +19 -12
  24. package/demo/export-chart.html +12 -9
  25. package/demo/family-tree.html +14 -11
  26. package/demo/familytree-custom-properties.html +108 -0
  27. package/demo/filter-node.html +104 -57
  28. package/demo/get-hierarchy.html +17 -12
  29. package/demo/get-related-nodes.html +27 -17
  30. package/demo/index.html +5 -2
  31. package/demo/integrate-map.html +32 -27
  32. package/demo/left2right.html +12 -9
  33. package/demo/level-offset.html +16 -15
  34. package/demo/link-node.html +12 -9
  35. package/demo/local-datasource.html +11 -8
  36. package/demo/minimap.html +224 -0
  37. package/demo/nodes-of-different-widths.html +11 -8
  38. package/demo/ondemand-loading-data.html +88 -91
  39. package/demo/option-createNode.html +28 -17
  40. package/demo/pan-zoom.html +20 -11
  41. package/demo/reload-data.html +17 -14
  42. package/demo/report-path.html +59 -29
  43. package/demo/responsive-design.html +14 -11
  44. package/demo/right2left.html +12 -9
  45. package/demo/toggle-sibs-resp.html +12 -9
  46. package/demo/ul-datasource.html +11 -8
  47. package/demo/vertical-level.html +12 -9
  48. package/dist/css/{jquery.orgchart.css → orgchart.css} +143 -80
  49. package/dist/css/orgchart.min.css +1 -0
  50. package/dist/js/orgchart.js +4101 -0
  51. package/dist/js/orgchart.min.js +2 -0
  52. package/dist/js/orgchart.min.js.map +1 -0
  53. package/gulpfile.js +37 -26
  54. package/package.json +32 -32
  55. package/src/css/{jquery.orgchart.css → orgchart.css} +143 -80
  56. package/src/js/orgchart.js +4101 -0
  57. package/test/cypress/e2e/demo-interactions.cy.js +91 -0
  58. package/test/cypress/e2e/demo-rendering.cy.js +100 -0
  59. package/test/cypress/e2e/drag-drop-hybrid-chart.cy.js +22 -0
  60. package/test/cypress/e2e/edit-chart.cy.js +20 -0
  61. package/test/cypress/e2e/level-offset.cy.js +21 -0
  62. package/test/cypress/e2e/minimap.cy.js +66 -0
  63. package/test/cypress/e2e/pan-zoom.cy.js +165 -0
  64. package/test/cypress/e2e/report-path.cy.js +20 -0
  65. package/test/cypress/e2e/vertical-vevel.cy.js +5 -3
  66. package/test/e2e/local-datasource/test.js +1 -1
  67. package/test/integration/init-tests.js +168 -62
  68. package/test/integration/tests.js +1034 -56
  69. package/test/unit/lifecycle-builders-export.tests.js +1426 -0
  70. package/test/unit/minimap.tests.js +210 -0
  71. package/test/unit/tests.js +2372 -252
  72. package/dist/css/jquery.orgchart.min.css +0 -1
  73. package/dist/js/jquery.orgchart.js +0 -1741
  74. package/dist/js/jquery.orgchart.min.js +0 -5
  75. package/dist/js/jquery.orgchart.min.js.map +0 -1
  76. package/src/js/jquery.orgchart.js +0 -1741
@@ -1,16 +1,63 @@
1
- var chai = require('chai');
2
- var sinon = require('sinon');
3
- var sinonChai = require('sinon-chai');
4
- var should = chai.should();
1
+ const chai = require('chai');
2
+ const sinon = require('sinon');
3
+ const sinonChai = require('sinon-chai').default;
4
+ const should = chai.should();
5
5
  chai.use(sinonChai);
6
6
  require('jsdom-global')();
7
- var $ = require('jquery');
8
- require('../../src/js/jquery.orgchart');
7
+ process.env.ORGCHART_TEST = '1';
8
+ const OrgChart = require('../../src/js/orgchart');
9
+ const getState = OrgChart.__testing__.getState;
10
+ const setState = OrgChart.__testing__.setState;
11
+
12
+ function query(selector, root) {
13
+ return (root || document).querySelector(selector);
14
+ }
15
+
16
+ function queryAll(selector, root) {
17
+ return Array.from((root || document).querySelectorAll(selector));
18
+ }
19
+
20
+ function siblingElements(element, selector) {
21
+ if (!element || !element.parentElement) {
22
+ return [];
23
+ }
24
+ return Array.from(element.parentElement.children).filter(function (childEl) {
25
+ return childEl !== element && (!selector || childEl.matches(selector));
26
+ });
27
+ }
28
+
29
+ function cloneValue(value) {
30
+ return JSON.parse(JSON.stringify(value));
31
+ }
32
+
33
+ function dispatchNativeEvent(element, type, options) {
34
+ const eventOptions = Object.assign({ bubbles: true }, options);
35
+ let event;
36
+
37
+ if (/click|mouse/.test(type)) {
38
+ event = new window.MouseEvent(type, eventOptions);
39
+ } else {
40
+ event = new window.Event(type, eventOptions);
41
+ }
42
+ element.dispatchEvent(event);
43
+ }
44
+
45
+ function dispatchEventWithProperties(element, type, properties, options) {
46
+ const event = new window.Event(type, Object.assign({ bubbles: true }, options));
47
+
48
+ Object.keys(properties || {}).forEach(function (key) {
49
+ Object.defineProperty(event, key, {
50
+ configurable: true,
51
+ value: properties[key]
52
+ });
53
+ });
54
+ element.dispatchEvent(event);
55
+ }
9
56
 
10
57
  describe('orgchart -- integration tests', function () {
11
- document.body.innerHTML = '<div id="chart-container"></div>';
12
- var $container = $('#chart-container'),
13
- ds = {
58
+ let $container;
59
+
60
+ const ds = {
14
61
  'id': 'n1',
15
62
  'name': 'Lao Lao',
16
63
  'children': [
@@ -36,9 +83,11 @@ describe('orgchart -- integration tests', function () {
36
83
  },
37
84
  { 'id': 'n4', 'name': 'Hong Miao' },
38
85
  ]
39
- },
40
- oc = {},
41
- hierarchy = {
86
+ };
87
+
88
+ let oc = {};
89
+
90
+ const hierarchy = {
42
91
  id: 'n1',
43
92
  children: [
44
93
  { id: 'n2' },
@@ -63,91 +112,1020 @@ describe('orgchart -- integration tests', function () {
63
112
  },
64
113
  { id: 'n4' }
65
114
  ]
66
- },
67
- $laolao,
68
- $bomiao,
69
- $sumiao,
70
- $hongmiao,
71
- $chunmiao,
72
- $tiehua,
73
- $heihei,
74
- $pangpang,
75
- $dandan,
76
- $erdan;
115
+ };
116
+
117
+ let $laolao;
118
+ let $bomiao;
119
+ let $sumiao;
120
+ let $hongmiao;
121
+ let $chunmiao;
122
+ let $tiehua;
123
+ let $heihei;
124
+ let $pangpang;
125
+ let $dandan;
126
+ let $erdan;
77
127
 
78
128
  beforeEach(function () {
79
- oc = $container.orgchart({
129
+ document.body.innerHTML = '<div id="chart-container"></div>';
130
+ $container = document.getElementById('chart-container');
131
+ oc = new OrgChart({
132
+ chartContainer: '#chart-container',
80
133
  'data': ds
81
134
  }),
82
- $laolao = $('#n1'),
83
- $bomiao = $('#n2'),
84
- $sumiao = $('#n3'),
85
- $hongmiao = $('#n4'),
86
- $tiehua = $('#n5'),
87
- $heihei = $('#n6'),
88
- $pangpang = $('#n7'),
89
- $dandan = $('#n8'),
90
- $erdan = $('#n9'),
91
- $sandan = $('#n10');
92
- });
93
-
135
+ $laolao = document.getElementById('n1'),
136
+ $bomiao = document.getElementById('n2'),
137
+ $sumiao = document.getElementById('n3'),
138
+ $hongmiao = document.getElementById('n4'),
139
+ $tiehua = document.getElementById('n5'),
140
+ $heihei = document.getElementById('n6'),
141
+ $pangpang = document.getElementById('n7'),
142
+ $dandan = document.getElementById('n8'),
143
+ $erdan = document.getElementById('n9'),
144
+ $sandan = document.getElementById('n10');
145
+ });
146
+
94
147
  afterEach(function () {
95
148
  $laolao = $bomiao = $sumiao = $hongmiao = $chunmiao = $tiehua = $heihei = $pangpang = $dandan = $erdan = $sandan = null;
96
- $container.empty();
149
+ $container.innerHTML = '';
97
150
  });
98
151
 
99
152
  it('addParent()', function () {
153
+ const expectedHierarchy = {
154
+ id: 'n0',
155
+ children: [cloneValue(hierarchy)]
156
+ };
157
+
100
158
  oc.addParent($laolao, { 'name': 'Lao Ye', 'id': 'n0' });
101
- $laolao.closest('.nodes').siblings('.node').should.lengthOf(1);
102
- oc.$chart.find('.node:first').should.deep.equal($('#n0'));
159
+
160
+ siblingElements($laolao.closest('.nodes'), '.node').should.lengthOf(1);
161
+ query('.node', oc.chart).should.equal(query('#n0'));
162
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
163
+ });
164
+
165
+ it('inserts a parent between a non-root node and its existing parent', function () {
166
+ const expectedHierarchy = cloneValue(hierarchy);
167
+
168
+ expectedHierarchy.children[0] = {
169
+ id: 'n0',
170
+ children: [expectedHierarchy.children[0]]
171
+ };
172
+ oc.addParent($bomiao, { 'name': 'Lao Ye', 'id': 'n0' });
173
+
174
+ query('#n0').closest('.hierarchy').parentElement.parentElement.querySelector(':scope > .node').should.equal(query('#n1'));
175
+ query('#n2').closest('.hierarchy').parentElement.previousElementSibling.should.equal(query('#n0'));
176
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
177
+ });
178
+
179
+ it('addAncestors()', function () {
180
+ oc.addAncestors({ 'name': 'Lao Ye', 'id': 'n0', 'relationship': '001' }, 'n0');
181
+
182
+ should.exist(query('#n0'));
183
+ query('#n0').closest('.hierarchy').should.not.equal(query('#n1').closest('.hierarchy'));
184
+ oc.getHierarchy().should.deep.equal(hierarchy);
103
185
  });
104
186
 
105
187
  describe('addChildren()', function () {
106
188
  it('Add child nodes to the leaf node', function () {
189
+ let siblingNodesElement;
190
+ const expectedHierarchy = cloneValue(hierarchy);
191
+
192
+ expectedHierarchy.children[0].children = [{ id: 'n11' }];
193
+
107
194
  oc.addChildren($bomiao, [{'name': 'Li Xin', 'id': 'n11' }]);
108
- $bomiao.siblings('.nodes').should.lengthOf(1);
109
- $bomiao.siblings('.nodes').find('.hierarchy').should.lengthOf(1);
110
- $bomiao.siblings('.nodes').find('.node').attr('id').should.equal('n11');
195
+ siblingNodesElement = siblingElements($bomiao, '.nodes')[0];
196
+ siblingElements($bomiao, '.nodes').should.lengthOf(1);
197
+ queryAll('.hierarchy', siblingNodesElement).should.lengthOf(1);
198
+ query('.node', siblingNodesElement).id.should.equal('n11');
199
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
111
200
  });
112
201
 
113
202
  it('Add child nodes to the un-leaf node', function () {
203
+ let siblingNodesElements;
204
+ let hierarchyElements;
205
+ const expectedHierarchy = cloneValue(hierarchy);
206
+
207
+ expectedHierarchy.children[1].children.push({ id: 'n11' });
208
+
114
209
  oc.addChildren($sumiao, [{'name': 'Li Xin', 'id': 'n11' }]);
115
- $sumiao.siblings('.nodes').children('.hierarchy').should.lengthOf(4);
116
- $sumiao.siblings('.nodes').children('.hierarchy:last').find('.node').attr('id').should.equal('n11');
210
+ siblingNodesElements = siblingElements($sumiao, '.nodes');
211
+ hierarchyElements = siblingNodesElements.flatMap(function (nodesElement) {
212
+ return Array.from(nodesElement.children).filter(function (childEl) {
213
+ return childEl.classList && childEl.classList.contains('hierarchy');
214
+ });
215
+ });
216
+ hierarchyElements.should.lengthOf(4);
217
+ query('.node', hierarchyElements[hierarchyElements.length - 1]).id.should.equal('n11');
218
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
219
+ });
220
+ });
221
+
222
+ describe('addDescendants()', function () {
223
+ it('Add descendant nodes from a native parent node', function () {
224
+ let siblingNodesElement;
225
+ const expectedHierarchy = cloneValue(hierarchy);
226
+
227
+ expectedHierarchy.children[0].children = [{ id: 'n11' }];
228
+
229
+ oc.addDescendants([{ 'name': 'Li Xin', 'id': 'n11' }], $bomiao);
230
+
231
+ siblingNodesElement = siblingElements($bomiao, '.nodes')[0];
232
+ siblingElements($bomiao, '.nodes').should.lengthOf(1);
233
+ queryAll('.hierarchy', siblingNodesElement).should.lengthOf(1);
234
+ query('.node', siblingNodesElement).id.should.equal('n11');
235
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
236
+ });
237
+
238
+ it('Preserves descendant insertion order and nested descendant structure', function () {
239
+ const expectedHierarchy = cloneValue(hierarchy);
240
+
241
+ expectedHierarchy.children[0].children = [
242
+ { id: 'n11', children: [{ id: 'n12' }] },
243
+ { id: 'n13' }
244
+ ];
245
+
246
+ oc.addDescendants([
247
+ {
248
+ 'name': 'Li Xin',
249
+ 'id': 'n11',
250
+ 'children': [
251
+ { 'name': 'Xiao Xin', 'id': 'n12' }
252
+ ]
253
+ },
254
+ { 'name': 'Li Na', 'id': 'n13' }
255
+ ], $bomiao);
256
+
257
+ Array.from($bomiao.nextElementSibling.children).filter(function (childEl) {
258
+ return childEl.classList && childEl.classList.contains('hierarchy');
259
+ }).should.lengthOf(2);
260
+ queryAll('#n12', $bomiao.nextElementSibling).should.lengthOf(1);
261
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
117
262
  });
118
263
  });
119
264
 
120
265
  describe('addSiblings()', function () {
121
266
  it('Just add sibling nodes', function () {
267
+ const expectedHierarchy = cloneValue(hierarchy);
268
+ const rootHierarchyElement = $laolao.closest('.hierarchy');
269
+ const rootChildrenContainer = Array.from(rootHierarchyElement.children).find(function (childEl) {
270
+ return childEl.classList && childEl.classList.contains('nodes');
271
+ });
272
+
273
+ expectedHierarchy.children.push({ id: 'n11' });
274
+
122
275
  oc.addSiblings($sumiao, [{'name': 'Li Xin', 'id': 'n11' }]);
123
- $laolao.siblings('.nodes').children('.hierarchy').should.lengthOf(4);
124
- $laolao.siblings('.nodes').children('.hierarchy:last').find('.node').attr('id').should.equal('n11');
276
+ Array.from(rootChildrenContainer.children).filter(function (childEl) {
277
+ return childEl.classList && childEl.classList.contains('hierarchy');
278
+ }).should.lengthOf(4);
279
+ should.exist(query('#n11', rootChildrenContainer));
280
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
125
281
  });
126
282
 
127
283
  it('Add sibling nodes as well as parent node', function () {
284
+ let hierarchySiblings;
285
+ const expectedHierarchy = {
286
+ id: 'n0',
287
+ children: [{ id: 'n11' }, cloneValue(hierarchy)]
288
+ };
289
+
128
290
  oc.addSiblings($laolao, { 'name': 'Lao Ye', 'id': 'n0', 'children': [{'name': 'Li Xin', 'id': 'n11' }] });
129
- $laolao.closest('.nodes').siblings('.node').should.lengthOf(1);
130
- oc.$chart.find('.node:first').should.deep.equal($('#n0'));
131
- $laolao.closest('.hierarchy').siblings().should.lengthOf(1);
132
- $laolao.closest('.hierarchy').siblings().find('.node').attr('id').should.equal('n11');
291
+ siblingElements($laolao.closest('.nodes'), '.node').should.lengthOf(1);
292
+ query('.node', oc.chart).should.equal(query('#n0'));
293
+ hierarchySiblings = siblingElements($laolao.closest('.hierarchy'));
294
+ hierarchySiblings.should.lengthOf(1);
295
+ query('.node', hierarchySiblings[0]).id.should.equal('n11');
296
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
133
297
  });
134
298
  });
135
299
 
136
300
  describe('removeNodes()', function () {
137
301
  it('Remove leaf node', function () {
302
+ const expectedHierarchy = cloneValue(hierarchy);
303
+
304
+ delete expectedHierarchy.children[1].children[0].children;
305
+
138
306
  oc.removeNodes($dandan);
139
- $tiehua.siblings('.nodes').should.lengthOf(0);
307
+ siblingElements($tiehua, '.nodes').should.lengthOf(0);
308
+ should.not.exist(query('.bottomEdge', $tiehua));
309
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
140
310
  });
311
+
141
312
  it('Remove parent node', function () {
313
+ const siblingNodesElement = siblingElements($sumiao, '.nodes')[0];
314
+ const expectedHierarchy = cloneValue(hierarchy);
315
+
316
+ expectedHierarchy.children[1].children.splice(0, 1);
317
+
142
318
  oc.removeNodes($tiehua);
143
- $sumiao.siblings('.nodes').children('.hierarchy').should.lengthOf(2);
144
- $('#n5').should.lengthOf(0);
145
- $('#n8').should.lengthOf(0);
319
+ Array.from(siblingNodesElement.children).filter(function (childEl) {
320
+ return childEl.classList && childEl.classList.contains('hierarchy');
321
+ }).should.lengthOf(2);
322
+ should.not.exist(query('#n5'));
323
+ should.not.exist(query('#n8'));
324
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
325
+ });
326
+
327
+ it('Remove a root child node and preserve sibling order', function () {
328
+ const expectedHierarchy = cloneValue(hierarchy);
329
+ const rootHierarchyElement = $laolao.closest('.hierarchy');
330
+ const rootChildrenContainer = Array.from(rootHierarchyElement.children).find(function (childEl) {
331
+ return childEl.classList && childEl.classList.contains('nodes');
332
+ });
333
+
334
+ expectedHierarchy.children.splice(0, 1);
335
+
336
+ oc.removeNodes($bomiao);
337
+
338
+ should.not.exist(query('#n2'));
339
+ oc.getHierarchy().should.deep.equal(expectedHierarchy);
340
+ Array.from(query(':scope > .nodes', oc.chart).children).filter(function (childEl) {
341
+ return childEl.classList && childEl.classList.contains('hierarchy');
342
+ }).map(function (hierarchyEl) {
343
+ return query('.node', hierarchyEl).id;
344
+ }).should.deep.equal(['n1']);
345
+ Array.from(rootChildrenContainer.children).filter(function (childEl) {
346
+ return childEl.classList && childEl.classList.contains('hierarchy');
347
+ }).map(function (hierarchyEl) {
348
+ return query('.node', hierarchyEl).id;
349
+ }).should.deep.equal(['n3', 'n4']);
146
350
  });
351
+
147
352
  it('Remove root node', function () {
148
353
  oc.removeNodes($laolao);
149
- oc.$chartContainer.is(':empty').should.be.true;
354
+ $container.children.length.should.equal(0);
150
355
  });
151
356
  });
152
357
 
153
- });
358
+ describe('interaction flows', function () {
359
+ it('dispatches hide/show children events through bottom-edge clicks', function () {
360
+ const bottomEdge = query('.bottomEdge', $sumiao);
361
+ const firstChildNode = siblingElements($sumiao, '.nodes')[0].querySelector('.node');
362
+ const events = [];
363
+
364
+ $sumiao.addEventListener('hide-children.orgchart', function (event) {
365
+ events.push(event.type);
366
+ });
367
+ $sumiao.addEventListener('show-children.orgchart', function (event) {
368
+ events.push(event.type);
369
+ });
370
+
371
+ dispatchNativeEvent(bottomEdge, 'click');
372
+ dispatchNativeEvent(firstChildNode, 'transitionend');
373
+ dispatchNativeEvent(bottomEdge, 'click');
374
+ dispatchNativeEvent(firstChildNode, 'transitionend');
375
+
376
+ events.should.deep.equal(['hide-children.orgchart', 'show-children.orgchart']);
377
+ });
378
+
379
+ it('dispatches hide/show siblings events through horizontal-edge clicks', function () {
380
+ const leftEdge = query('.leftEdge', $sumiao);
381
+ const firstSiblingNode = query('#n2');
382
+ const events = [];
383
+
384
+ $sumiao.addEventListener('hide-siblings.orgchart', function (event) {
385
+ events.push(event.type);
386
+ });
387
+ $sumiao.addEventListener('show-siblings.orgchart', function (event) {
388
+ events.push(event.type);
389
+ });
390
+
391
+ dispatchNativeEvent(leftEdge, 'click');
392
+ dispatchNativeEvent(firstSiblingNode, 'transitionend');
393
+ dispatchNativeEvent(leftEdge, 'click');
394
+ dispatchNativeEvent(firstSiblingNode, 'transitionend');
395
+
396
+ events.should.deep.equal(['hide-siblings.orgchart', 'show-siblings.orgchart']);
397
+ });
398
+
399
+ it('dispatches hide-parent and enters ancestor-collapsed state through top-edge click', function () {
400
+ const topEdge = query('.topEdge', $heihei);
401
+ const hideSpy = sinon.spy();
402
+ const currentHierarchy = $heihei.closest('.hierarchy');
403
+ const leftSiblingHierarchy = currentHierarchy.previousElementSibling;
404
+ const rightSiblingHierarchy = currentHierarchy.nextElementSibling;
405
+
406
+ $heihei.addEventListener('hide-parent.orgchart', hideSpy);
407
+
408
+ dispatchNativeEvent(topEdge, 'click');
409
+
410
+ hideSpy.should.have.been.calledOnce;
411
+ hideSpy.firstCall.args[0].type.should.equal('hide-parent.orgchart');
412
+ currentHierarchy.classList.contains('isAncestorsCollapsed').should.be.true;
413
+ currentHierarchy.classList.contains('isSiblingsCollapsed').should.be.true;
414
+ leftSiblingHierarchy.classList.contains('isCollapsedSibling').should.be.true;
415
+ rightSiblingHierarchy.classList.contains('isCollapsedSibling').should.be.true;
416
+ $sumiao.classList.contains('sliding').should.be.true;
417
+ $sumiao.classList.contains('slide-down').should.be.true;
418
+ $laolao.classList.contains('sliding').should.be.true;
419
+ $laolao.classList.contains('slide-down').should.be.true;
420
+
421
+ dispatchNativeEvent($sumiao, 'transitionend');
422
+ dispatchNativeEvent($laolao, 'transitionend');
423
+
424
+ $sumiao.classList.contains('sliding').should.be.false;
425
+ $laolao.classList.contains('sliding').should.be.false;
426
+ });
427
+
428
+ it('focuses the clicked node and clears focus when clicking chart background', function () {
429
+ dispatchNativeEvent($sumiao, 'click');
430
+ $sumiao.classList.contains('focused').should.be.true;
431
+
432
+ dispatchNativeEvent($hongmiao, 'click');
433
+ $sumiao.classList.contains('focused').should.be.false;
434
+ $hongmiao.classList.contains('focused').should.be.true;
435
+
436
+ dispatchNativeEvent(oc.chart, 'click');
437
+ queryAll('.node.focused', oc.chart).should.have.lengthOf(0);
438
+ });
439
+
440
+ it('toggles children visibility through bottom-edge clicks and transition cleanup', function () {
441
+ const childNodesContainer = siblingElements($sumiao, '.nodes')[0];
442
+ const bottomEdge = query('.bottomEdge', $sumiao);
443
+ const childHierarchies = Array.from(childNodesContainer.children).filter(function (childEl) {
444
+ return childEl.classList && childEl.classList.contains('hierarchy');
445
+ });
446
+ const visibleChildNodes = childHierarchies.map(function (hierarchyEl) {
447
+ return query('.node', hierarchyEl);
448
+ });
449
+
450
+ oc.getNodeState($sumiao, 'children').should.deep.equal({ exist: true, visible: true });
451
+
452
+ dispatchNativeEvent(bottomEdge, 'click');
453
+ childNodesContainer.classList.contains('hidden').should.be.false;
454
+ childHierarchies.forEach(function (hierarchyEl) {
455
+ hierarchyEl.classList.contains('isCollapsedDescendant').should.be.true;
456
+ });
457
+ visibleChildNodes.forEach(function (childNodeEl) {
458
+ childNodeEl.classList.contains('sliding').should.be.true;
459
+ childNodeEl.classList.contains('slide-up').should.be.true;
460
+ });
461
+
462
+ dispatchNativeEvent(visibleChildNodes[0], 'transitionend');
463
+ childNodesContainer.classList.contains('hidden').should.be.true;
464
+ oc.getNodeState($sumiao, 'children').should.deep.equal({ exist: true, visible: false });
465
+
466
+ dispatchNativeEvent(bottomEdge, 'click');
467
+ childNodesContainer.classList.contains('hidden').should.be.false;
468
+ visibleChildNodes.forEach(function (childNodeEl) {
469
+ childNodeEl.classList.contains('sliding').should.be.true;
470
+ childNodeEl.classList.contains('slide-up').should.be.false;
471
+ });
472
+
473
+ dispatchNativeEvent(visibleChildNodes[0], 'transitionend');
474
+ oc.getNodeState($sumiao, 'children').should.deep.equal({ exist: true, visible: true });
475
+ });
476
+
477
+ it('toggles sibling visibility through horizontal-edge clicks and transition cleanup', function () {
478
+ const leftEdge = query('.leftEdge', $sumiao);
479
+ const siblingHierarchies = Array.from($sumiao.closest('.hierarchy').parentElement.children).filter(function (childEl) {
480
+ return childEl !== $sumiao.closest('.hierarchy') && childEl.classList.contains('hierarchy');
481
+ });
482
+ const siblingNodes = siblingHierarchies.map(function (hierarchyEl) {
483
+ return query('.node', hierarchyEl);
484
+ });
485
+
486
+ oc.getNodeState($sumiao, 'siblings').should.deep.equal({ exist: true, visible: true });
487
+
488
+ dispatchNativeEvent(leftEdge, 'click');
489
+ siblingNodes.forEach(function (siblingNodeEl) {
490
+ siblingNodeEl.classList.contains('sliding').should.be.true;
491
+ });
492
+
493
+ dispatchNativeEvent(siblingNodes[0], 'transitionend');
494
+ siblingHierarchies.forEach(function (siblingHierarchyEl) {
495
+ siblingHierarchyEl.classList.contains('hidden').should.be.true;
496
+ });
497
+ oc.getNodeState($sumiao, 'siblings').should.deep.equal({ exist: true, visible: false });
498
+
499
+ dispatchNativeEvent(leftEdge, 'click');
500
+ siblingHierarchies.forEach(function (siblingHierarchyEl) {
501
+ siblingHierarchyEl.classList.contains('hidden').should.be.false;
502
+ });
503
+ siblingNodes.forEach(function (siblingNodeEl) {
504
+ siblingNodeEl.classList.contains('sliding').should.be.true;
505
+ });
506
+
507
+ dispatchNativeEvent(siblingNodes[0], 'transitionend');
508
+ oc.getNodeState($sumiao, 'siblings').should.deep.equal({ exist: true, visible: true });
509
+ });
510
+
511
+ it('shows directional edge icons on hover and clears them on mouseleave', function () {
512
+ const topEdge = query('.topEdge', $sumiao);
513
+ const bottomEdge = query('.bottomEdge', $sumiao);
514
+ const leftEdge = query('.leftEdge', $sumiao);
515
+ const rightEdge = query('.rightEdge', $sumiao);
516
+
517
+ dispatchNativeEvent($sumiao, 'mouseenter', { bubbles: false });
518
+ topEdge.classList.contains(oc.options.icons.collapseToDown).should.be.true;
519
+ bottomEdge.classList.contains(oc.options.icons.expandToUp).should.be.true;
520
+ leftEdge.classList.contains(oc.options.icons.expandToRight).should.be.true;
521
+ rightEdge.classList.contains(oc.options.icons.collapseToLeft).should.be.true;
522
+
523
+ dispatchNativeEvent($sumiao, 'mouseleave', { bubbles: false });
524
+ topEdge.classList.contains(oc.options.icons.expandToUp).should.be.false;
525
+ topEdge.classList.contains(oc.options.icons.collapseToDown).should.be.false;
526
+ bottomEdge.classList.contains(oc.options.icons.expandToUp).should.be.false;
527
+ bottomEdge.classList.contains(oc.options.icons.collapseToDown).should.be.false;
528
+ leftEdge.classList.contains(oc.options.icons.expandToRight).should.be.false;
529
+ leftEdge.classList.contains(oc.options.icons.collapseToLeft).should.be.false;
530
+ rightEdge.classList.contains(oc.options.icons.expandToRight).should.be.false;
531
+ rightEdge.classList.contains(oc.options.icons.collapseToLeft).should.be.false;
532
+ });
533
+ });
534
+
535
+ describe('query methods', function () {
536
+ it('returns current parent, children, and siblings as native nodes', function () {
537
+ oc.getParent($sumiao).should.equal($laolao);
538
+ oc.getChildren($sumiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n5', 'n6', 'n7']);
539
+ oc.getSiblings($sumiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n2', 'n4']);
540
+ });
541
+
542
+ it('updates relationship queries after tree mutations', function () {
543
+ oc.addChildren($bomiao, [{ name: 'Li Xin', id: 'n11' }]);
544
+ oc.addSiblings($sumiao, [{ name: 'Li Na', id: 'n12' }]);
545
+
546
+ should.exist(query('#n11'));
547
+ should.exist(query('#n12'));
548
+ oc.getChildren($bomiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n11']);
549
+ oc.getSiblings($sumiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n2', 'n4', 'n12']);
550
+ oc.getParent(query('#n11')).should.equal($bomiao);
551
+ oc.getParent(query('#n12')).should.equal($laolao);
552
+ });
553
+ });
554
+
555
+ describe('runtime option flows', function () {
556
+ it('rebuilds the rendered hierarchy through setOptions({ data })', function () {
557
+ const replacementData = {
558
+ id: 'r1',
559
+ name: 'Replacement Root',
560
+ children: [{ id: 'r2', name: 'Replacement Child' }]
561
+ };
562
+ const previousChart = oc.chart;
563
+
564
+ oc.setOptions({ data: replacementData });
565
+
566
+ oc.chart.should.not.equal(previousChart);
567
+ should.not.exist(query('#n1', $container));
568
+ query('#r1', oc.chart).querySelector('.title').textContent.should.equal('Replacement Root');
569
+ oc.getChildren(query('#r1', oc.chart)).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['r2']);
570
+ oc.getHierarchy().should.deep.equal({ id: 'r1', children: [{ id: 'r2' }] });
571
+ });
572
+
573
+ it('moves the chart through the minimap viewport and cleans up when disabled', function () {
574
+ const minimapChart = new OrgChart({
575
+ chartContainer: '#chart-container',
576
+ data: ds,
577
+ minimap: true
578
+ });
579
+ const minimapElement = query('.orgchart-minimap', $container);
580
+ const minimapStage = query('.orgchart-minimap-stage', minimapElement);
581
+ const minimapViewport = query('.orgchart-minimap-viewport', minimapElement);
582
+ const containerRectStub = sinon.stub(minimapChart.chartContainer, 'getBoundingClientRect').returns({
583
+ left: 0, top: 0, right: 400, bottom: 240, width: 400, height: 240
584
+ });
585
+ const chartRectStub = sinon.stub(minimapChart.chart, 'getBoundingClientRect').returns({
586
+ left: -100, top: -50, right: 900, bottom: 550, width: 1000, height: 600
587
+ });
588
+ const stageRectStub = sinon.stub(minimapStage, 'getBoundingClientRect').returns({
589
+ left: 0, top: 0, right: 180, bottom: 104, width: 180, height: 104
590
+ });
591
+ const viewportRectStub = sinon.stub(minimapViewport, 'getBoundingClientRect').returns({
592
+ left: 20, top: 10, right: 89, bottom: 52, width: 69, height: 42
593
+ });
594
+
595
+ Object.defineProperty(minimapStage, 'clientWidth', { configurable: true, value: 180 });
596
+ Object.defineProperty(minimapStage, 'clientHeight', { configurable: true, value: 104 });
597
+ minimapChart.updateMinimap(true);
598
+
599
+ dispatchEventWithProperties(minimapViewport, 'mousedown', {
600
+ clientX: 20,
601
+ clientY: 10,
602
+ preventDefault: function () {}
603
+ });
604
+ dispatchEventWithProperties(document, 'mousemove', { clientX: 40, clientY: 30 });
605
+ dispatchNativeEvent(document, 'mouseup');
606
+
607
+ minimapChart.chart.style.transform.should.not.equal('');
608
+ should.equal(getState(minimapChart.chart, 'minimapDragState'), null);
609
+
610
+ minimapChart.setOptions('minimap', false);
611
+
612
+ should.not.exist(query('.orgchart-minimap', $container));
613
+ $container.style.position.should.equal('');
614
+ containerRectStub.restore();
615
+ chartRectStub.restore();
616
+ stageRectStub.restore();
617
+ viewportRectStub.restore();
618
+ });
619
+ });
620
+
621
+ describe('hybrid vertical flows', function () {
622
+ it('expands and collapses vertical descendants through toggle button clicks', function () {
623
+ let hybridChart;
624
+ let hybridSumiao;
625
+ let toggleBtn;
626
+ let descendantsWrapper;
627
+ let childNodes;
628
+
629
+ $container.innerHTML = '';
630
+ hybridChart = new OrgChart({
631
+ chartContainer: '#chart-container',
632
+ data: {
633
+ id: 'n1',
634
+ name: 'Lao Lao',
635
+ children: [
636
+ { id: 'n2', name: 'Bo Miao' },
637
+ {
638
+ id: 'n3',
639
+ name: 'Su Miao',
640
+ children: [
641
+ { id: 'n5', name: 'Tie Hua' },
642
+ { id: 'n6', name: 'Hei Hei' }
643
+ ]
644
+ }
645
+ ]
646
+ },
647
+ verticalLevel: 2,
648
+ visibleLevel: 2
649
+ });
650
+ hybridSumiao = query('#n3', hybridChart.chart);
651
+ toggleBtn = query('.toggleBtn', hybridSumiao);
652
+ descendantsWrapper = siblingElements(hybridSumiao, '.nodes')[0];
653
+ childNodes = Array.from(descendantsWrapper.children).filter(function (childEl) {
654
+ return childEl.classList && childEl.classList.contains('hierarchy');
655
+ }).map(function (hierarchyEl) {
656
+ return query('.node', hierarchyEl);
657
+ });
658
+
659
+ should.exist(toggleBtn);
660
+ descendantsWrapper.classList.contains('hidden').should.be.true;
661
+ childNodes.forEach(function (childNodeEl) {
662
+ childNodeEl.classList.contains('slide-up').should.be.true;
663
+ });
664
+
665
+ dispatchNativeEvent(toggleBtn, 'click');
666
+ descendantsWrapper.classList.contains('hidden').should.be.false;
667
+ childNodes.forEach(function (childNodeEl) {
668
+ childNodeEl.classList.contains('sliding').should.be.true;
669
+ childNodeEl.classList.contains('slide-up').should.be.false;
670
+ });
671
+
672
+ dispatchNativeEvent(childNodes[0], 'transitionend');
673
+ childNodes.forEach(function (childNodeEl) {
674
+ childNodeEl.classList.contains('sliding').should.be.false;
675
+ });
676
+
677
+ dispatchNativeEvent(toggleBtn, 'click');
678
+ descendantsWrapper.classList.contains('hidden').should.be.false;
679
+ childNodes.forEach(function (childNodeEl) {
680
+ childNodeEl.classList.contains('sliding').should.be.true;
681
+ childNodeEl.classList.contains('slide-up').should.be.true;
682
+ });
683
+
684
+ dispatchNativeEvent(childNodes[0], 'transitionend');
685
+ descendantsWrapper.classList.contains('hidden').should.be.true;
686
+ childNodes.forEach(function (childNodeEl) {
687
+ childNodeEl.classList.contains('sliding').should.be.false;
688
+ });
689
+ });
690
+ });
691
+
692
+ describe('drag and drop flows', function () {
693
+ it('filters allowed drop zones through the public dropCriteria option', function () {
694
+ const draggableChart = new OrgChart({
695
+ chartContainer: '#chart-container',
696
+ data: ds,
697
+ draggable: true,
698
+ dropCriteria: function (draggedNode, draggedParentNode, dropNode) {
699
+ return draggedNode.id === 'n2' && draggedParentNode.id === 'n1' && dropNode.id === 'n4';
700
+ }
701
+ });
702
+ const draggableBomiao = query('#n2', draggableChart.chart);
703
+
704
+ dispatchEventWithProperties(draggableBomiao, 'dragstart', {
705
+ dataTransfer: {
706
+ setData: function () {}
707
+ }
708
+ });
709
+
710
+ queryAll('.allowedDrop', draggableChart.chart).map(function (nodeEl) {
711
+ return nodeEl.id;
712
+ }).should.deep.equal(['n4']);
713
+
714
+ dispatchNativeEvent(draggableBomiao, 'dragend');
715
+ queryAll('.allowedDrop', draggableChart.chart).should.have.lengthOf(0);
716
+ });
717
+
718
+ it('dispatches otherdropped for an external dragged item', async function () {
719
+ const draggableChart = new OrgChart({
720
+ chartContainer: '#chart-container',
721
+ data: ds,
722
+ draggable: true
723
+ });
724
+ const draggableBomiao = query('#n2', draggableChart.chart);
725
+ const foreignItem = document.createElement('div');
726
+ const dropSpy = sinon.spy();
727
+
728
+ foreignItem.className = 'foreign-item';
729
+ draggableChart.chart.addEventListener('otherdropped.orgchart', dropSpy);
730
+
731
+ dispatchEventWithProperties(draggableBomiao, 'dragstart', {
732
+ dataTransfer: {
733
+ setData: function () {}
734
+ }
735
+ });
736
+ setState(draggableChart.chart, 'dragged', foreignItem);
737
+
738
+ await draggableChart.dropHandler({
739
+ dropZone: draggableBomiao,
740
+ originalEvent: {
741
+ currentTarget: draggableBomiao
742
+ }
743
+ });
744
+
745
+ dropSpy.should.have.been.calledOnce;
746
+ dropSpy.firstCall.args[0].type.should.equal('otherdropped.orgchart');
747
+ dropSpy.firstCall.args[0].detail.draggedItem.should.equal(foreignItem);
748
+ dropSpy.firstCall.args[0].detail.dropZone.should.equal(draggableBomiao);
749
+ });
750
+
751
+ it('marks allowed drop zones on native dragstart and clears them on dragend', function () {
752
+ const draggableChart = new OrgChart({
753
+ chartContainer: '#chart-container',
754
+ data: ds,
755
+ draggable: true
756
+ });
757
+ const draggableBomiao = query('#n2', draggableChart.chart);
758
+ const draggableHongmiao = query('#n4', draggableChart.chart);
759
+
760
+ dispatchEventWithProperties(draggableBomiao, 'dragstart', {
761
+ dataTransfer: {
762
+ setData: function () {}
763
+ }
764
+ });
765
+
766
+ draggableHongmiao.classList.contains('allowedDrop').should.be.true;
767
+ queryAll('.allowedDrop', draggableChart.chart).length.should.be.above(0);
768
+
769
+ dispatchNativeEvent(draggableBomiao, 'dragend');
770
+
771
+ queryAll('.allowedDrop', draggableChart.chart).should.have.lengthOf(0);
772
+ });
773
+
774
+ it('dispatches nodedrop and moves the dragged node through native drag/drop events', async function () {
775
+ const originalJSONDigger = global.JSONDigger;
776
+ const draggableChart = new OrgChart({
777
+ chartContainer: '#chart-container',
778
+ data: ds,
779
+ draggable: true
780
+ });
781
+ const draggableBomiao = query('#n2', draggableChart.chart);
782
+ const draggableHongmiao = query('#n4', draggableChart.chart);
783
+ const rootChildrenContainer = siblingElements(query('#n1', draggableChart.chart), '.nodes')[0];
784
+ const dropSpy = sinon.spy();
785
+
786
+ global.JSONDigger = function () {
787
+ return {
788
+ findOneNode: function () {
789
+ return null;
790
+ }
791
+ };
792
+ };
793
+ draggableChart.chart.addEventListener('nodedrop.orgchart', dropSpy);
794
+
795
+ dispatchEventWithProperties(draggableBomiao, 'dragstart', {
796
+ dataTransfer: {
797
+ setData: function () {}
798
+ }
799
+ });
800
+ draggableHongmiao.classList.add('allowedDrop');
801
+
802
+ await draggableChart.dropHandler({
803
+ dropZone: draggableHongmiao,
804
+ originalEvent: {
805
+ currentTarget: draggableHongmiao
806
+ }
807
+ });
808
+
809
+ dropSpy.should.have.been.calledOnce;
810
+ dropSpy.firstCall.args[0].type.should.equal('nodedrop.orgchart');
811
+ dropSpy.firstCall.args[0].detail.draggedNode.should.equal(draggableBomiao);
812
+ dropSpy.firstCall.args[0].detail.dropZone.should.equal(draggableHongmiao);
813
+ queryAll('#n2', siblingElements(draggableHongmiao, '.nodes')[0]).should.have.lengthOf(1);
814
+ Array.from(rootChildrenContainer.children).filter(function (childEl) {
815
+ return childEl.classList && childEl.classList.contains('hierarchy');
816
+ }).map(function (hierarchyEl) {
817
+ return query('.node', hierarchyEl).id;
818
+ }).should.deep.equal(['n3', 'n4']);
819
+
820
+ global.JSONDigger = originalJSONDigger;
821
+ });
822
+
823
+ it('adds a vertical toggle when hybrid drag and drop gives a leaf node children', async function () {
824
+ const originalJSONDigger = global.JSONDigger;
825
+ const hybridData = {
826
+ id: 'n1',
827
+ name: 'Root',
828
+ children: [
829
+ {
830
+ id: 'n2',
831
+ name: 'Source Parent',
832
+ children: [{ id: 'n3', name: 'Dragged Node' }]
833
+ },
834
+ { id: 'n4', name: 'Drop Node' }
835
+ ]
836
+ };
837
+ const hybridChart = new OrgChart({
838
+ chartContainer: '#chart-container',
839
+ data: hybridData,
840
+ draggable: true,
841
+ verticalLevel: 2
842
+ });
843
+ const draggedNode = query('#n3', hybridChart.chart);
844
+ const dropNode = query('#n4', hybridChart.chart);
845
+
846
+ global.JSONDigger = require('json-digger');
847
+ try {
848
+ setState(hybridChart.chart, 'dragged', draggedNode);
849
+ dropNode.classList.add('allowedDrop');
850
+ await hybridChart.dropHandler({
851
+ dropZone: dropNode,
852
+ originalEvent: { currentTarget: dropNode }
853
+ });
854
+
855
+ query('#n4', hybridChart.chart).querySelector(':scope > .toggleBtn').should.exist;
856
+ query('#n3', hybridChart.chart).closest('.hierarchy').parentElement.previousElementSibling.id.should.equal('n4');
857
+ } finally {
858
+ global.JSONDigger = originalJSONDigger;
859
+ }
860
+ });
861
+ });
862
+
863
+ describe('compact mode flows', function () {
864
+ it('toggles loose and compact modes through compact-node controls', function () {
865
+ let compactChart;
866
+ let compactNode;
867
+ let backToLooseSymbol;
868
+ let backToCompactSymbol;
869
+
870
+ $container.innerHTML = '';
871
+ compactChart = new OrgChart({
872
+ chartContainer: '#chart-container',
873
+ data: {
874
+ id: 'n1',
875
+ name: 'Lao Lao',
876
+ children: [
877
+ {
878
+ id: 'n2',
879
+ name: 'Bo Miao',
880
+ compact: true,
881
+ children: [
882
+ { id: 'n3', name: 'Su Miao' },
883
+ { id: 'n4', name: 'Hong Miao' }
884
+ ]
885
+ }
886
+ ]
887
+ }
888
+ });
889
+ compactNode = query('#n2', compactChart.chart);
890
+ backToLooseSymbol = query('.backToLooseSymbol', compactNode);
891
+ backToCompactSymbol = query('.backToCompactSymbol', compactNode);
892
+
893
+ should.exist(backToLooseSymbol);
894
+ should.exist(backToCompactSymbol);
895
+ compactNode.classList.contains('looseMode').should.be.false;
896
+ backToCompactSymbol.classList.contains('hidden').should.be.true;
897
+
898
+ dispatchNativeEvent(backToLooseSymbol, 'click');
899
+
900
+ compactNode.classList.contains('looseMode').should.be.true;
901
+ backToLooseSymbol.classList.contains('hidden').should.be.true;
902
+ backToCompactSymbol.classList.contains('hidden').should.be.false;
903
+
904
+ dispatchNativeEvent(backToCompactSymbol, 'click');
905
+
906
+ compactNode.classList.contains('looseMode').should.be.false;
907
+ backToLooseSymbol.classList.contains('hidden').should.be.false;
908
+ backToCompactSymbol.classList.contains('hidden').should.be.true;
909
+ });
910
+ });
911
+
912
+ describe('export flows', function () {
913
+ it('invokes export() through the rendered export button click', function () {
914
+ let exportChart;
915
+ let exportButton;
916
+ let exportStub;
917
+
918
+ $container.innerHTML = '';
919
+ Array.from(document.querySelectorAll('.oc-export-btn')).forEach(function (buttonEl) {
920
+ buttonEl.remove();
921
+ });
922
+ exportChart = new OrgChart({
923
+ chartContainer: '#chart-container',
924
+ data: ds,
925
+ exportButton: true
926
+ });
927
+ exportButton = document.querySelector('.oc-export-btn');
928
+ exportStub = sinon.stub(exportChart, 'export');
929
+
930
+ should.exist(exportButton);
931
+
932
+ dispatchNativeEvent(exportButton, 'click');
933
+
934
+ exportStub.should.have.been.calledOnce;
935
+ exportButton.remove();
936
+ exportStub.restore();
937
+ });
938
+ });
939
+
940
+ describe('pan and zoom binding flows', function () {
941
+ it('pans through native mousedown, mousemove, and mouseup bindings', function () {
942
+ let panChart;
943
+ let moveEvent;
944
+
945
+ $container.innerHTML = '';
946
+ panChart = new OrgChart({
947
+ chartContainer: '#chart-container',
948
+ data: ds,
949
+ pan: true
950
+ });
951
+
952
+ dispatchEventWithProperties(panChart.chart, 'mousedown', {
953
+ target: panChart.chart,
954
+ pageX: 10,
955
+ pageY: 20
956
+ });
957
+
958
+ getState(panChart.chart, 'panning').should.equal(true);
959
+ moveEvent = new window.Event('mousemove', { bubbles: true });
960
+ Object.defineProperty(moveEvent, 'pageX', { configurable: true, value: 30 });
961
+ Object.defineProperty(moveEvent, 'pageY', { configurable: true, value: 45 });
962
+ panChart.chart.dispatchEvent(moveEvent);
963
+ panChart.chart.style.transform.should.equal('matrix(1, 0, 0, 1, 20, 25)');
964
+
965
+ dispatchNativeEvent(document, 'mouseup');
966
+
967
+ getState(panChart.chart, 'panning').should.equal(false);
968
+ panChart.chart.style.cursor.should.equal('default');
969
+ });
970
+
971
+ it('zooms through the bound wheel listener on the chart container', function () {
972
+ let zoomChart;
973
+
974
+ $container.innerHTML = '';
975
+ zoomChart = new OrgChart({
976
+ chartContainer: '#chart-container',
977
+ data: ds,
978
+ zoom: true
979
+ });
980
+ zoomChart.chart.getBoundingClientRect = function () {
981
+ return { left: 0, top: 0 };
982
+ };
983
+ zoomChart.chartContainer.getBoundingClientRect = function () {
984
+ return {
985
+ left: 0,
986
+ top: 0,
987
+ right: 200,
988
+ bottom: 100,
989
+ width: 200,
990
+ height: 100
991
+ };
992
+ };
993
+
994
+ dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', {
995
+ deltaY: -1,
996
+ clientX: 60,
997
+ clientY: 45,
998
+ preventDefault: function () {}
999
+ });
1000
+
1001
+ zoomChart.chart.style.transform.should.equal('matrix(1.2, 0, 0, 1.2, -12, -9)');
1002
+ });
1003
+
1004
+ it('pinch-zooms through bound touchstart, touchmove, and touchend listeners', function () {
1005
+ let zoomChart;
1006
+
1007
+ $container.innerHTML = '';
1008
+ zoomChart = new OrgChart({
1009
+ chartContainer: '#chart-container',
1010
+ data: ds,
1011
+ zoom: true
1012
+ });
1013
+ zoomChart.chart.getBoundingClientRect = function () {
1014
+ return { left: 0, top: 0 };
1015
+ };
1016
+ zoomChart.chartContainer.getBoundingClientRect = function () {
1017
+ return {
1018
+ left: 0,
1019
+ top: 0,
1020
+ right: 200,
1021
+ bottom: 100,
1022
+ width: 200,
1023
+ height: 100
1024
+ };
1025
+ };
1026
+
1027
+ dispatchEventWithProperties(zoomChart.chartContainer, 'touchstart', {
1028
+ touches: [
1029
+ { clientX: 40, clientY: 40 },
1030
+ { clientX: 80, clientY: 40 }
1031
+ ]
1032
+ });
1033
+ dispatchEventWithProperties(document, 'touchmove', {
1034
+ touches: [
1035
+ { clientX: 30, clientY: 40 },
1036
+ { clientX: 90, clientY: 40 }
1037
+ ]
1038
+ });
1039
+ dispatchNativeEvent(document, 'touchend');
1040
+
1041
+ zoomChart.chart.style.transform.should.equal('matrix(1.2, 0, 0, 1.2, -12, -8)');
1042
+ });
1043
+
1044
+ it('toggles pan bindings through setOptions()', function () {
1045
+ let panChart;
1046
+
1047
+ $container.innerHTML = '';
1048
+ panChart = new OrgChart({
1049
+ chartContainer: '#chart-container',
1050
+ data: ds
1051
+ });
1052
+ setState(panChart.chart, 'panning', false);
1053
+
1054
+ dispatchEventWithProperties(panChart.chart, 'mousedown', {
1055
+ target: panChart.chart,
1056
+ pageX: 10,
1057
+ pageY: 20
1058
+ });
1059
+ getState(panChart.chart, 'panning').should.equal(false);
1060
+
1061
+ panChart.setOptions({ pan: true });
1062
+ dispatchEventWithProperties(panChart.chart, 'mousedown', {
1063
+ target: panChart.chart,
1064
+ pageX: 10,
1065
+ pageY: 20
1066
+ });
1067
+ getState(panChart.chart, 'panning').should.equal(true);
1068
+
1069
+ dispatchNativeEvent(document, 'mouseup');
1070
+ panChart.setOptions({ pan: false });
1071
+ setState(panChart.chart, 'panning', false);
1072
+
1073
+ dispatchEventWithProperties(panChart.chart, 'mousedown', {
1074
+ target: panChart.chart,
1075
+ pageX: 30,
1076
+ pageY: 40
1077
+ });
1078
+ getState(panChart.chart, 'panning').should.equal(false);
1079
+ });
1080
+
1081
+ it('toggles zoom bindings through setOptions()', function () {
1082
+ let zoomChart;
1083
+
1084
+ $container.innerHTML = '';
1085
+ zoomChart = new OrgChart({
1086
+ chartContainer: '#chart-container',
1087
+ data: ds
1088
+ });
1089
+ zoomChart.chart.getBoundingClientRect = function () {
1090
+ return { left: 0, top: 0 };
1091
+ };
1092
+ zoomChart.chartContainer.getBoundingClientRect = function () {
1093
+ return {
1094
+ left: 0,
1095
+ top: 0,
1096
+ right: 200,
1097
+ bottom: 100,
1098
+ width: 200,
1099
+ height: 100
1100
+ };
1101
+ };
1102
+
1103
+ dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', {
1104
+ deltaY: -1,
1105
+ clientX: 60,
1106
+ clientY: 45,
1107
+ preventDefault: function () {}
1108
+ });
1109
+ zoomChart.chart.style.transform.should.equal('');
1110
+
1111
+ zoomChart.setOptions({ zoom: true });
1112
+ dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', {
1113
+ deltaY: -1,
1114
+ clientX: 60,
1115
+ clientY: 45,
1116
+ preventDefault: function () {}
1117
+ });
1118
+ zoomChart.chart.style.transform.should.equal('matrix(1.2, 0, 0, 1.2, -12, -9)');
1119
+
1120
+ zoomChart.setOptions({ zoom: false });
1121
+ zoomChart.chart.style.transform = 'none';
1122
+ dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', {
1123
+ deltaY: -1,
1124
+ clientX: 60,
1125
+ clientY: 45,
1126
+ preventDefault: function () {}
1127
+ });
1128
+ zoomChart.chart.style.transform.should.equal('none');
1129
+ });
1130
+ });
1131
+ });