UNPKG

114 kBJavaScriptView Raw
1/**
2 * @license BSD
3 * @copyright 2014-2023 hizzgdev@163.com
4 *
5 * Project Home:
6 * https://github.com/hizzgdev/jsmind/
7 */
8
9; (function ($w) {
10 'use strict';
11 console.warn("The version is outdated. see details: https://hizzgdev.github.io/jsmind/es6/")
12 // set 'jsMind' as the library name.
13 // __name__ should be a const value, Never try to change it easily.
14 var __name__ = 'jsMind';
15 // library version
16 var __version__ = '0.5.7';
17 // author
18 var __author__ = 'hizzgdev@163.com';
19
20 // an noop function define
21 var _noop = function () { };
22 var logger = (typeof console === 'undefined') ? {
23 log: _noop, debug: _noop, error: _noop, warn: _noop, info: _noop
24 } : console;
25
26 // check global variables
27 if (typeof module === 'undefined' || !module.exports) {
28 if (typeof $w[__name__] != 'undefined') {
29 logger.log(__name__ + ' has been already exist.');
30 return;
31 }
32 }
33
34 // shortcut of methods in dom
35 var $d = $w.document;
36 var $g = function (id) { return $d.getElementById(id); };
37 var $c = function (tag) { return $d.createElement(tag); };
38 var $t = function (n, t) { if (n.hasChildNodes()) { n.firstChild.nodeValue = t; } else { n.appendChild($d.createTextNode(t)); } };
39
40 var $h = function (n, t) {
41 if (t instanceof HTMLElement) {
42 n.innerHTML = '';
43 n.appendChild(t);
44 } else {
45 n.innerHTML = t;
46 }
47 };
48 // detect isElement
49 var $i = function (el) { return !!el && (typeof el === 'object') && (el.nodeType === 1) && (typeof el.style === 'object') && (typeof el.ownerDocument === 'object'); };
50 if (typeof String.prototype.startsWith != 'function') { String.prototype.startsWith = function (p) { return this.slice(0, p.length) === p; }; }
51
52 var DEFAULT_OPTIONS = {
53 container: '', // id of the container
54 editable: false, // you can change it in your options
55 theme: null,
56 mode: 'full', // full or side
57 support_html: true,
58
59 view: {
60 engine: 'canvas',
61 hmargin: 100,
62 vmargin: 50,
63 line_width: 2,
64 line_color: '#555',
65 draggable: false, // drag the mind map with your mouse, when it's larger that the container
66 hide_scrollbars_when_draggable: false, // hide container scrollbars, when mind map is larger than container and draggable option is true.
67 node_overflow: 'hidden' // hidden or wrap
68 },
69 layout: {
70 hspace: 30,
71 vspace: 20,
72 pspace: 13,
73 cousin_space: 0
74 },
75 default_event_handle: {
76 enable_mousedown_handle: true,
77 enable_click_handle: true,
78 enable_dblclick_handle: true,
79 enable_mousewheel_handle: true
80 },
81 shortcut: {
82 enable: true,
83 handles: {
84 },
85 mapping: {
86 addchild: [45, 4096+13], // Insert, Ctrl+Enter
87 addbrother: 13, // Enter
88 editnode: 113,// F2
89 delnode: 46, // Delete
90 toggle: 32, // Space
91 left: 37, // Left
92 up: 38, // Up
93 right: 39, // Right
94 down: 40, // Down
95 }
96 },
97 };
98
99 // core object
100 var jm = function (options) {
101 jm.current = this;
102
103 this.version = __version__;
104 var opts = {};
105 jm.util.json.merge(opts, DEFAULT_OPTIONS);
106 jm.util.json.merge(opts, options);
107
108 if (!opts.container) {
109 logger.error('the options.container should not be null or empty.');
110 return;
111 }
112 this.options = opts;
113 this.initialized = false;
114 this.mind = null;
115 this.event_handles = [];
116 this.init();
117 };
118
119 // ============= static object =============================================
120 jm.direction = {
121 left: -1, center: 0, right: 1, of: function (dir) {
122 if (!dir || dir === -1 || dir === 0 || dir === 1) {
123 return dir;
124 }
125 if (dir === '-1' || dir === '0' || dir === '1') {
126 return parseInt(dir);
127 }
128 if (dir.toLowerCase() === 'left') {
129 return this.left;
130 }
131 if (dir.toLowerCase() === 'right') {
132 return this.right;
133 }
134 if (dir.toLowerCase() === 'center') {
135 return this.center;
136 }
137 }
138 };
139 jm.event_type = { show: 1, resize: 2, edit: 3, select: 4 };
140 jm.key = { meta: 1 << 13, ctrl: 1 << 12, alt: 1 << 11, shift: 1 << 10 };
141
142 jm.node = function (sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
143 if (!sId) { logger.error('invalid node id'); return; }
144 if (typeof iIndex != 'number') { logger.error('invalid node index'); return; }
145 if (typeof bExpanded === 'undefined') { bExpanded = true; }
146 this.id = sId;
147 this.index = iIndex;
148 this.topic = sTopic;
149 this.data = oData || {};
150 this.isroot = bIsRoot;
151 this.parent = oParent;
152 this.direction = eDirection;
153 this.expanded = !!bExpanded;
154 this.children = [];
155 this._data = {};
156 };
157
158 jm.node.compare = function (node1, node2) {
159 // '-1' is alwary the last
160 var r = 0;
161 var i1 = node1.index;
162 var i2 = node2.index;
163 if (i1 >= 0 && i2 >= 0) {
164 r = i1 - i2;
165 } else if (i1 == -1 && i2 == -1) {
166 r = 0;
167 } else if (i1 == -1) {
168 r = 1;
169 } else if (i2 == -1) {
170 r = -1;
171 } else {
172 r = 0;
173 }
174 //logger.debug(i1+' <> '+i2+' = '+r);
175 return r;
176 };
177
178 jm.node.inherited = function (pnode, node) {
179 if (!!pnode && !!node) {
180 if (pnode.id === node.id) {
181 return true;
182 }
183 if (pnode.isroot) {
184 return true;
185 }
186 var pid = pnode.id;
187 var p = node;
188 while (!p.isroot) {
189 p = p.parent;
190 if (p.id === pid) {
191 return true;
192 }
193 }
194 }
195 return false;
196 };
197
198 jm.node.is_node = function (n) {
199 return !!n && n instanceof jm.node;
200 };
201
202 jm.node.prototype = {
203 get_location: function () {
204 var vd = this._data.view;
205 return {
206 x: vd.abs_x,
207 y: vd.abs_y
208 };
209 },
210 get_size: function () {
211 var vd = this._data.view;
212 return {
213 w: vd.width,
214 h: vd.height
215 }
216 }
217 };
218
219
220 jm.mind = function () {
221 this.name = null;
222 this.author = null;
223 this.version = null;
224 this.root = null;
225 this.selected = null;
226 this.nodes = {};
227 };
228
229 jm.mind.prototype = {
230 get_node: function (nodeid) {
231 if (nodeid in this.nodes) {
232 return this.nodes[nodeid];
233 } else {
234 logger.warn('the node[id=' + nodeid + '] can not be found');
235 return null;
236 }
237 },
238
239 set_root: function (nodeid, topic, data) {
240 if (this.root == null) {
241 this.root = new jm.node(nodeid, 0, topic, data, true);
242 this._put_node(this.root);
243 return this.root;
244 } else {
245 logger.error('root node is already exist');
246 return null;
247 }
248 },
249
250 add_node: function (parent_node, nodeid, topic, data, direction, expanded, idx) {
251 if (!jm.util.is_node(parent_node)) {
252 logger.error('the parent_node ' + parent_node + ' is not a node.');
253 return null;
254 }
255 var node_index = idx || -1;
256 var node = new jm.node(nodeid, node_index, topic, data, false, parent_node, parent_node.direction, expanded);
257 if (parent_node.isroot) {
258 node.direction = direction || jm.direction.right;
259 }
260 if (this._put_node(node)) {
261 parent_node.children.push(node);
262 this._reindex(parent_node);
263 } else {
264 logger.error('fail, the nodeid \'' + node.id + '\' has been already exist.');
265 node = null;
266 }
267 return node;
268 },
269
270 insert_node_before: function (node_before, nodeid, topic, data, direction) {
271 if (!jm.util.is_node(node_before)) {
272 logger.error('the node_before ' + node_before + ' is not a node.');
273 return null;
274 }
275 var node_index = node_before.index - 0.5;
276 return this.add_node(node_before.parent, nodeid, topic, data, direction, true, node_index);
277 },
278
279 get_node_before: function (node) {
280 if (!jm.util.is_node(node)) {
281 var the_node = this.get_node(node);
282 if (!the_node) {
283 logger.error('the node[id=' + node + '] can not be found.');
284 return null;
285 } else {
286 return this.get_node_before(the_node);
287 }
288 }
289 if (node.isroot) { return null; }
290 var idx = node.index - 2;
291 if (idx >= 0) {
292 return node.parent.children[idx];
293 } else {
294 return null;
295 }
296 },
297
298 insert_node_after: function (node_after, nodeid, topic, data, direction) {
299 if (!jm.util.is_node(node_after)) {
300 logger.error('the node_after ' + node_after + ' is not a node.');
301 return null;
302 }
303 var node_index = node_after.index + 0.5;
304 return this.add_node(node_after.parent, nodeid, topic, data, direction, true, node_index);
305 },
306
307 get_node_after: function (node) {
308 if (!jm.util.is_node(node)) {
309 var the_node = this.get_node(node);
310 if (!the_node) {
311 logger.error('the node[id=' + node + '] can not be found.');
312 return null;
313 } else {
314 return this.get_node_after(the_node);
315 }
316 }
317 if (node.isroot) { return null; }
318 var idx = node.index;
319 var brothers = node.parent.children;
320 if (brothers.length > idx) {
321 return node.parent.children[idx];
322 } else {
323 return null;
324 }
325 },
326
327 move_node: function (node, before_id, parent_id, direction) {
328 if (!jm.util.is_node(node)) {
329 logger.error('the parameter node ' + node + ' is not a node.');
330 return null;
331 }
332 if (!parent_id) {
333 parent_id = node.parent.id;
334 }
335 return this._move_node(node, before_id, parent_id, direction);
336 },
337
338 _flow_node_direction: function (node, direction) {
339 if (typeof direction === 'undefined') {
340 direction = node.direction;
341 } else {
342 node.direction = direction;
343 }
344 var len = node.children.length;
345 while (len--) {
346 this._flow_node_direction(node.children[len], direction);
347 }
348 },
349
350 _move_node_internal: function (node, beforeid) {
351 if (!!node && !!beforeid) {
352 if (beforeid == '_last_') {
353 node.index = -1;
354 this._reindex(node.parent);
355 } else if (beforeid == '_first_') {
356 node.index = 0;
357 this._reindex(node.parent);
358 } else {
359 var node_before = (!!beforeid) ? this.get_node(beforeid) : null;
360 if (node_before != null && node_before.parent != null && node_before.parent.id == node.parent.id) {
361 node.index = node_before.index - 0.5;
362 this._reindex(node.parent);
363 }
364 }
365 }
366 return node;
367 },
368
369 _move_node: function (node, beforeid, parentid, direction) {
370 if (!!node && !!parentid) {
371 var parent_node = this.get_node(parentid)
372 if (jm.node.inherited(node, parent_node)) {
373 logger.error('can not move a node to its children');
374 return null;
375 }
376 if (node.parent.id != parentid) {
377 // remove from parent's children
378 var sibling = node.parent.children;
379 var si = sibling.length;
380 while (si--) {
381 if (sibling[si].id == node.id) {
382 sibling.splice(si, 1);
383 break;
384 }
385 }
386 node.parent = parent_node;
387 parent_node.children.push(node);
388 }
389
390 if (node.parent.isroot) {
391 if (direction == jm.direction.left) {
392 node.direction = direction;
393 } else {
394 node.direction = jm.direction.right;
395 }
396 } else {
397 node.direction = node.parent.direction;
398 }
399 this._move_node_internal(node, beforeid);
400 this._flow_node_direction(node);
401 }
402 return node;
403 },
404
405 remove_node: function (node) {
406 if (!jm.util.is_node(node)) {
407 logger.error('the parameter node ' + node + ' is not a node.');
408 return false;
409 }
410 if (node.isroot) {
411 logger.error('fail, can not remove root node');
412 return false;
413 }
414 if (this.selected != null && this.selected.id == node.id) {
415 this.selected = null;
416 }
417 // clean all subordinate nodes
418 var children = node.children;
419 var ci = children.length;
420 while (ci--) {
421 this.remove_node(children[ci]);
422 }
423 // clean all children
424 children.length = 0;
425 // remove from parent's children
426 var sibling = node.parent.children;
427 var si = sibling.length;
428 while (si--) {
429 if (sibling[si].id == node.id) {
430 sibling.splice(si, 1);
431 break;
432 }
433 }
434 // remove from global nodes
435 delete this.nodes[node.id];
436 // clean all properties
437 for (var k in node) {
438 delete node[k];
439 }
440 // remove it's self
441 node = null;
442 //delete node;
443 return true;
444 },
445
446 _put_node: function (node) {
447 if (node.id in this.nodes) {
448 logger.warn('the nodeid \'' + node.id + '\' has been already exist.');
449 return false;
450 } else {
451 this.nodes[node.id] = node;
452 return true;
453 }
454 },
455
456 _reindex: function (node) {
457 if (node instanceof jm.node) {
458 node.children.sort(jm.node.compare);
459 for (var i = 0; i < node.children.length; i++) {
460 node.children[i].index = i + 1;
461 }
462 }
463 },
464 };
465
466 jm.format = {
467 node_tree: {
468 example: {
469 "meta": {
470 "name": __name__,
471 "author": __author__,
472 "version": __version__
473 },
474 "format": "node_tree",
475 "data": { "id": "root", "topic": "jsMind Example" }
476 },
477 get_mind: function (source) {
478 var df = jm.format.node_tree;
479 var mind = new jm.mind();
480 mind.name = source.meta.name;
481 mind.author = source.meta.author;
482 mind.version = source.meta.version;
483 df._parse(mind, source.data);
484 return mind;
485 },
486 get_data: function (mind) {
487 var df = jm.format.node_tree;
488 var json = {};
489 json.meta = {
490 name: mind.name,
491 author: mind.author,
492 version: mind.version
493 };
494 json.format = 'node_tree';
495 json.data = df._buildnode(mind.root);
496 return json;
497 },
498
499 _parse: function (mind, node_root) {
500 var df = jm.format.node_tree;
501 var data = df._extract_data(node_root);
502 mind.set_root(node_root.id, node_root.topic, data);
503 if ('children' in node_root) {
504 var children = node_root.children;
505 for (var i = 0; i < children.length; i++) {
506 df._extract_subnode(mind, mind.root, children[i]);
507 }
508 }
509 },
510
511 _extract_data: function (node_json) {
512 var data = {};
513 for (var k in node_json) {
514 if (k == 'id' || k == 'topic' || k == 'children' || k == 'direction' || k == 'expanded') {
515 continue;
516 }
517 data[k] = node_json[k];
518 }
519 return data;
520 },
521
522 _extract_subnode: function (mind, node_parent, node_json) {
523 var df = jm.format.node_tree;
524 var data = df._extract_data(node_json);
525 var d = null;
526 if (node_parent.isroot) {
527 d = node_json.direction == 'left' ? jm.direction.left : jm.direction.right;
528 }
529 var node = mind.add_node(node_parent, node_json.id, node_json.topic, data, d, node_json.expanded);
530 if (!!node_json['children']) {
531 var children = node_json.children;
532 for (var i = 0; i < children.length; i++) {
533 df._extract_subnode(mind, node, children[i]);
534 }
535 }
536 },
537
538 _buildnode: function (node) {
539 var df = jm.format.node_tree;
540 if (!(node instanceof jm.node)) { return; }
541 var o = {
542 id: node.id,
543 topic: node.topic,
544 expanded: node.expanded
545 };
546 if (!!node.parent && node.parent.isroot) {
547 o.direction = node.direction == jm.direction.left ? 'left' : 'right';
548 }
549 if (node.data != null) {
550 var node_data = node.data;
551 for (var k in node_data) {
552 o[k] = node_data[k];
553 }
554 }
555 var children = node.children;
556 if (children.length > 0) {
557 o.children = [];
558 for (var i = 0; i < children.length; i++) {
559 o.children.push(df._buildnode(children[i]));
560 }
561 }
562 return o;
563 }
564 },
565
566 node_array: {
567 example: {
568 "meta": {
569 "name": __name__,
570 "author": __author__,
571 "version": __version__
572 },
573 "format": "node_array",
574 "data": [
575 { "id": "root", "topic": "jsMind Example", "isroot": true }
576 ]
577 },
578
579 get_mind: function (source) {
580 var df = jm.format.node_array;
581 var mind = new jm.mind();
582 mind.name = source.meta.name;
583 mind.author = source.meta.author;
584 mind.version = source.meta.version;
585 df._parse(mind, source.data);
586 return mind;
587 },
588
589 get_data: function (mind) {
590 var df = jm.format.node_array;
591 var json = {};
592 json.meta = {
593 name: mind.name,
594 author: mind.author,
595 version: mind.version
596 };
597 json.format = 'node_array';
598 json.data = [];
599 df._array(mind, json.data);
600 return json;
601 },
602
603 _parse: function (mind, node_array) {
604 var df = jm.format.node_array;
605 var narray = node_array.slice(0);
606 // reverse array for improving looping performance
607 narray.reverse();
608 var root_node = df._extract_root(mind, narray);
609 if (!!root_node) {
610 df._extract_subnode(mind, root_node, narray);
611 } else {
612 logger.error('root node can not be found');
613 }
614 },
615
616 _extract_root: function (mind, node_array) {
617 var df = jm.format.node_array;
618 var i = node_array.length;
619 while (i--) {
620 if ('isroot' in node_array[i] && node_array[i].isroot) {
621 var root_json = node_array[i];
622 var data = df._extract_data(root_json);
623 var node = mind.set_root(root_json.id, root_json.topic, data);
624 node_array.splice(i, 1);
625 return node;
626 }
627 }
628 return null;
629 },
630
631 _extract_subnode: function (mind, parent_node, node_array) {
632 var df = jm.format.node_array;
633 var i = node_array.length;
634 var node_json = null;
635 var data = null;
636 var extract_count = 0;
637 while (i--) {
638 node_json = node_array[i];
639 if (node_json.parentid == parent_node.id) {
640 data = df._extract_data(node_json);
641 var d = null;
642 var node_direction = node_json.direction;
643 if (!!node_direction) {
644 d = node_direction == 'left' ? jm.direction.left : jm.direction.right;
645 }
646 var node = mind.add_node(parent_node, node_json.id, node_json.topic, data, d, node_json.expanded);
647 node_array.splice(i, 1);
648 extract_count++;
649 var sub_extract_count = df._extract_subnode(mind, node, node_array);
650 if (sub_extract_count > 0) {
651 // reset loop index after extract subordinate node
652 i = node_array.length;
653 extract_count += sub_extract_count;
654 }
655 }
656 }
657 return extract_count;
658 },
659
660 _extract_data: function (node_json) {
661 var data = {};
662 for (var k in node_json) {
663 if (k == 'id' || k == 'topic' || k == 'parentid' || k == 'isroot' || k == 'direction' || k == 'expanded') {
664 continue;
665 }
666 data[k] = node_json[k];
667 }
668 return data;
669 },
670
671 _array: function (mind, node_array) {
672 var df = jm.format.node_array;
673 df._array_node(mind.root, node_array);
674 },
675
676 _array_node: function (node, node_array) {
677 var df = jm.format.node_array;
678 if (!(node instanceof jm.node)) { return; }
679 var o = {
680 id: node.id,
681 topic: node.topic,
682 expanded: node.expanded
683 };
684 if (!!node.parent) {
685 o.parentid = node.parent.id;
686 }
687 if (node.isroot) {
688 o.isroot = true;
689 }
690 if (!!node.parent && node.parent.isroot) {
691 o.direction = node.direction == jm.direction.left ? 'left' : 'right';
692 }
693 if (node.data != null) {
694 var node_data = node.data;
695 for (var k in node_data) {
696 o[k] = node_data[k];
697 }
698 }
699 node_array.push(o);
700 var ci = node.children.length;
701 for (var i = 0; i < ci; i++) {
702 df._array_node(node.children[i], node_array);
703 }
704 },
705 },
706
707 freemind: {
708 example: {
709 "meta": {
710 "name": __name__,
711 "author": __author__,
712 "version": __version__
713 },
714 "format": "freemind",
715 "data": "<map version=\"1.0.1\"><node ID=\"root\" TEXT=\"freemind Example\"/></map>"
716 },
717 get_mind: function (source) {
718 var df = jm.format.freemind;
719 var mind = new jm.mind();
720 mind.name = source.meta.name;
721 mind.author = source.meta.author;
722 mind.version = source.meta.version;
723 var xml = source.data;
724 var xml_doc = df._parse_xml(xml);
725 var xml_root = df._find_root(xml_doc);
726 df._load_node(mind, null, xml_root);
727 return mind;
728 },
729
730 get_data: function (mind) {
731 var df = jm.format.freemind;
732 var json = {};
733 json.meta = {
734 name: mind.name,
735 author: mind.author,
736 version: mind.version
737 };
738 json.format = 'freemind';
739 var xmllines = [];
740 xmllines.push('<map version=\"1.0.1\">');
741 df._buildmap(mind.root, xmllines);
742 xmllines.push('</map>');
743 json.data = xmllines.join(' ');
744 return json;
745 },
746
747 _parse_xml: function (xml) {
748 var xml_doc = null;
749 if (window.DOMParser) {
750 var parser = new DOMParser();
751 xml_doc = parser.parseFromString(xml, 'text/xml');
752 } else { // Internet Explorer
753 xml_doc = new ActiveXObject('Microsoft.XMLDOM');
754 xml_doc.async = false;
755 xml_doc.loadXML(xml);
756 }
757 return xml_doc;
758 },
759
760 _find_root: function (xml_doc) {
761 var nodes = xml_doc.childNodes;
762 var node = null;
763 var root = null;
764 var n = null;
765 for (var i = 0; i < nodes.length; i++) {
766 n = nodes[i];
767 if (n.nodeType == 1 && n.tagName == 'map') {
768 node = n;
769 break;
770 }
771 }
772 if (!!node) {
773 var ns = node.childNodes;
774 node = null;
775 for (var i = 0; i < ns.length; i++) {
776 n = ns[i];
777 if (n.nodeType == 1 && n.tagName == 'node') {
778 node = n;
779 break;
780 }
781 }
782 }
783 return node;
784 },
785
786 _load_node: function (mind, parent_node, xml_node) {
787 var df = jm.format.freemind;
788 var node_id = xml_node.getAttribute('ID');
789 var node_topic = xml_node.getAttribute('TEXT');
790 // look for richcontent
791 if (node_topic == null) {
792 var topic_children = xml_node.childNodes;
793 var topic_child = null;
794 for (var i = 0; i < topic_children.length; i++) {
795 topic_child = topic_children[i];
796 //logger.debug(topic_child.tagName);
797 if (topic_child.nodeType == 1 && topic_child.tagName === 'richcontent') {
798 node_topic = topic_child.textContent;
799 break;
800 }
801 }
802 }
803 var node_data = df._load_attributes(xml_node);
804 var node_expanded = ('expanded' in node_data) ? (node_data.expanded == 'true') : true;
805 delete node_data.expanded;
806
807 var node_position = xml_node.getAttribute('POSITION');
808 var node_direction = null;
809 if (!!node_position) {
810 node_direction = node_position == 'left' ? jm.direction.left : jm.direction.right;
811 }
812 //logger.debug(node_position +':'+ node_direction);
813 var node = null;
814 if (!!parent_node) {
815 node = mind.add_node(parent_node, node_id, node_topic, node_data, node_direction, node_expanded);
816 } else {
817 node = mind.set_root(node_id, node_topic, node_data);
818 }
819 var children = xml_node.childNodes;
820 var child = null;
821 for (var i = 0; i < children.length; i++) {
822 child = children[i];
823 if (child.nodeType == 1 && child.tagName == 'node') {
824 df._load_node(mind, node, child);
825 }
826 }
827 },
828
829 _load_attributes: function (xml_node) {
830 var children = xml_node.childNodes;
831 var attr = null;
832 var attr_data = {};
833 for (var i = 0; i < children.length; i++) {
834 attr = children[i];
835 if (attr.nodeType == 1 && attr.tagName === 'attribute') {
836 attr_data[attr.getAttribute('NAME')] = attr.getAttribute('VALUE');
837 }
838 }
839 return attr_data;
840 },
841
842 _buildmap: function (node, xmllines) {
843 var df = jm.format.freemind;
844 var pos = null;
845 if (!!node.parent && node.parent.isroot) {
846 pos = node.direction === jm.direction.left ? 'left' : 'right';
847 }
848 xmllines.push('<node');
849 xmllines.push('ID=\"' + node.id + '\"');
850 if (!!pos) {
851 xmllines.push('POSITION=\"' + pos + '\"');
852 }
853 xmllines.push('TEXT=\"' + node.topic + '\">');
854
855 // store expanded status as an attribute
856 xmllines.push('<attribute NAME=\"expanded\" VALUE=\"' + node.expanded + '\"/>');
857
858 // for attributes
859 var node_data = node.data;
860 if (node_data != null) {
861 for (var k in node_data) {
862 xmllines.push('<attribute NAME=\"' + k + '\" VALUE=\"' + node_data[k] + '\"/>');
863 }
864 }
865
866 // for children
867 var children = node.children;
868 for (var i = 0; i < children.length; i++) {
869 df._buildmap(children[i], xmllines);
870 }
871
872 xmllines.push('</node>');
873 },
874 },
875 };
876
877 // ============= utility object =============================================
878
879 jm.util = {
880 is_node: function (node) {
881 return !!node && node instanceof jm.node;
882 },
883 ajax: {
884 request: function (url, param, method, callback, fail_callback) {
885 var a = jm.util.ajax;
886 var p = null;
887 var tmp_param = [];
888 for (var k in param) {
889 tmp_param.push(encodeURIComponent(k) + '=' + encodeURIComponent(param[k]));
890 }
891 if (tmp_param.length > 0) {
892 p = tmp_param.join('&');
893 }
894 var xhr = new XMLHttpRequest();
895 if (!xhr) { return; }
896 xhr.onreadystatechange = function () {
897 if (xhr.readyState == 4) {
898 if (xhr.status == 200 || xhr.status == 0) {
899 if (typeof callback === 'function') {
900 var data = jm.util.json.string2json(xhr.responseText);
901 if (data != null) {
902 callback(data);
903 } else {
904 callback(xhr.responseText);
905 }
906 }
907 } else {
908 if (typeof fail_callback === 'function') {
909 fail_callback(xhr);
910 } else {
911 logger.error('xhr request failed.', xhr);
912 }
913 }
914 }
915 }
916 method = method || 'GET';
917 xhr.open(method, url, true);
918 xhr.setRequestHeader('If-Modified-Since', '0');
919 if (method == 'POST') {
920 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
921 xhr.send(p);
922 } else {
923 xhr.send();
924 }
925 },
926 get: function (url, callback) {
927 return jm.util.ajax.request(url, {}, 'GET', callback);
928 },
929 post: function (url, param, callback) {
930 return jm.util.ajax.request(url, param, 'POST', callback);
931 }
932 },
933
934 dom: {
935 //target,eventType,handler
936 add_event: function (t, e, h) {
937 if (!!t.addEventListener) {
938 t.addEventListener(e, h, false);
939 } else {
940 t.attachEvent('on' + e, h);
941 }
942 }
943 },
944
945 file: {
946 read: function (file_data, fn_callback) {
947 var reader = new FileReader();
948 reader.onload = function () {
949 if (typeof fn_callback === 'function') {
950 fn_callback(this.result, file_data.name);
951 }
952 };
953 reader.readAsText(file_data);
954 },
955
956 save: function (file_data, type, name) {
957 var blob;
958 if (typeof $w.Blob === 'function') {
959 blob = new Blob([file_data], { type: type });
960 } else {
961 var BlobBuilder = $w.BlobBuilder || $w.MozBlobBuilder || $w.WebKitBlobBuilder || $w.MSBlobBuilder;
962 var bb = new BlobBuilder();
963 bb.append(file_data);
964 blob = bb.getBlob(type);
965 }
966 if (navigator.msSaveBlob) {
967 navigator.msSaveBlob(blob, name);
968 } else {
969 var URL = $w.URL || $w.webkitURL;
970 var bloburl = URL.createObjectURL(blob);
971 var anchor = $c('a');
972 if ('download' in anchor) {
973 anchor.style.visibility = 'hidden';
974 anchor.href = bloburl;
975 anchor.download = name;
976 $d.body.appendChild(anchor);
977 var evt = $d.createEvent('MouseEvents');
978 evt.initEvent('click', true, true);
979 anchor.dispatchEvent(evt);
980 $d.body.removeChild(anchor);
981 } else {
982 location.href = bloburl;
983 }
984 }
985 }
986 },
987
988 json: {
989 json2string: function (json) {
990 return JSON.stringify(json);
991 },
992 string2json: function (json_str) {
993 return JSON.parse(json_str);
994 },
995 merge: function (b, a) {
996 for (var o in a) {
997 if (o in b) {
998 if (typeof b[o] === 'object' &&
999 Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
1000 !b[o].length) {
1001 jm.util.json.merge(b[o], a[o]);
1002 } else {
1003 b[o] = a[o];
1004 }
1005 } else {
1006 b[o] = a[o];
1007 }
1008 }
1009 return b;
1010 }
1011 },
1012
1013 uuid: {
1014 newid: function () {
1015 return (new Date().getTime().toString(16) + Math.random().toString(16).substring(2)).substring(2, 18);
1016 }
1017 },
1018
1019 text: {
1020 is_empty: function (s) {
1021 if (!s) { return true; }
1022 return s.replace(/\s*/, '').length == 0;
1023 }
1024 }
1025 };
1026
1027 jm.prototype = {
1028 init: function () {
1029 if (this.initialized) { return; }
1030 this.initialized = true;
1031
1032 var opts = this.options;
1033
1034 var opts_layout = {
1035 mode: opts.mode,
1036 hspace: opts.layout.hspace,
1037 vspace: opts.layout.vspace,
1038 pspace: opts.layout.pspace,
1039 cousin_space: opts.layout.cousin_space
1040 }
1041 var opts_view = {
1042 container: opts.container,
1043 support_html: opts.support_html,
1044 engine: opts.view.engine,
1045 hmargin: opts.view.hmargin,
1046 vmargin: opts.view.vmargin,
1047 line_width: opts.view.line_width,
1048 line_color: opts.view.line_color,
1049 draggable: opts.view.draggable,
1050 hide_scrollbars_when_draggable: opts.view.hide_scrollbars_when_draggable,
1051 node_overflow: opts.view.node_overflow
1052 };
1053 // create instance of function provider
1054 this.data = new jm.data_provider(this);
1055 this.layout = new jm.layout_provider(this, opts_layout);
1056 this.view = new jm.view_provider(this, opts_view);
1057 this.shortcut = new jm.shortcut_provider(this, opts.shortcut);
1058
1059 this.data.init();
1060 this.layout.init();
1061 this.view.init();
1062 this.shortcut.init();
1063
1064 this._event_bind();
1065
1066 jm.init_plugins(this);
1067 },
1068
1069 enable_edit: function () {
1070 this.options.editable = true;
1071 },
1072
1073 disable_edit: function () {
1074 this.options.editable = false;
1075 },
1076
1077 // call enable_event_handle('dblclick')
1078 // options are 'mousedown', 'click', 'dblclick'
1079 enable_event_handle: function (event_handle) {
1080 this.options.default_event_handle['enable_' + event_handle + '_handle'] = true;
1081 },
1082
1083 // call disable_event_handle('dblclick')
1084 // options are 'mousedown', 'click', 'dblclick'
1085 disable_event_handle: function (event_handle) {
1086 this.options.default_event_handle['enable_' + event_handle + '_handle'] = false;
1087 },
1088
1089 get_editable: function () {
1090 return this.options.editable;
1091 },
1092
1093 set_theme: function (theme) {
1094 var theme_old = this.options.theme;
1095 this.options.theme = (!!theme) ? theme : null;
1096 if (theme_old != this.options.theme) {
1097 this.view.reset_theme();
1098 this.view.reset_custom_style();
1099 }
1100 },
1101 _event_bind: function () {
1102 this.view.add_event(this, 'mousedown', this.mousedown_handle);
1103 this.view.add_event(this, 'click', this.click_handle);
1104 this.view.add_event(this, 'dblclick', this.dblclick_handle);
1105 this.view.add_event(this, "mousewheel", this.mousewheel_handle)
1106 },
1107
1108 mousedown_handle: function (e) {
1109 if (!this.options.default_event_handle['enable_mousedown_handle']) {
1110 return;
1111 }
1112 var element = e.target || event.srcElement;
1113 var nodeid = this.view.get_binded_nodeid(element);
1114 if (!!nodeid) {
1115 if (this.view.is_node(element)) {
1116 this.select_node(nodeid);
1117 }
1118 } else {
1119 this.select_clear();
1120 }
1121 },
1122
1123 click_handle: function (e) {
1124 if (!this.options.default_event_handle['enable_click_handle']) {
1125 return;
1126 }
1127 var element = e.target || event.srcElement;
1128 var is_expander = this.view.is_expander(element);
1129 if (is_expander) {
1130 var nodeid = this.view.get_binded_nodeid(element);
1131 if (!!nodeid) {
1132 this.toggle_node(nodeid);
1133 }
1134 }
1135 },
1136
1137 dblclick_handle: function (e) {
1138 if (!this.options.default_event_handle['enable_dblclick_handle']) {
1139 return;
1140 }
1141 if (this.get_editable()) {
1142 var element = e.target || event.srcElement;
1143 var is_node = this.view.is_node(element);
1144 if (is_node) {
1145 var nodeid = this.view.get_binded_nodeid(element);
1146 if (!!nodeid) {
1147 this.begin_edit(nodeid);
1148 }
1149 }
1150 }
1151 },
1152
1153 // Use [Ctrl] + Mousewheel, to zoom in/out.
1154 mousewheel_handle: function (event) {
1155 // Test if mousewheel option is enabled and Ctrl key is pressed.
1156 if (!this.options.default_event_handle["enable_mousewheel_handle"] || !window.event.ctrlKey) {
1157 return
1158 }
1159 // Avoid default page scrolling behavior.
1160 event.preventDefault()
1161
1162 if (event.deltaY < 0) {
1163 this.view.zoomIn()
1164 } else {
1165 this.view.zoomOut()
1166 }
1167 },
1168
1169 begin_edit: function (node) {
1170 if (!jm.util.is_node(node)) {
1171 var the_node = this.get_node(node);
1172 if (!the_node) {
1173 logger.error('the node[id=' + node + '] can not be found.');
1174 return false;
1175 } else {
1176 return this.begin_edit(the_node);
1177 }
1178 }
1179 if (this.get_editable()) {
1180 this.view.edit_node_begin(node);
1181 } else {
1182 logger.error('fail, this mind map is not editable.');
1183 return;
1184 }
1185 },
1186
1187 end_edit: function () {
1188 this.view.edit_node_end();
1189 },
1190
1191 toggle_node: function (node) {
1192 if (!jm.util.is_node(node)) {
1193 var the_node = this.get_node(node);
1194 if (!the_node) {
1195 logger.error('the node[id=' + node + '] can not be found.');
1196 return;
1197 } else {
1198 return this.toggle_node(the_node);
1199 }
1200 }
1201 if (node.isroot) { return; }
1202 this.view.save_location(node);
1203 this.layout.toggle_node(node);
1204 this.view.relayout();
1205 this.view.restore_location(node);
1206 },
1207
1208 expand_node: function (node) {
1209 if (!jm.util.is_node(node)) {
1210 var the_node = this.get_node(node);
1211 if (!the_node) {
1212 logger.error('the node[id=' + node + '] can not be found.');
1213 return;
1214 } else {
1215 return this.expand_node(the_node);
1216 }
1217 }
1218 if (node.isroot) { return; }
1219 this.view.save_location(node);
1220 this.layout.expand_node(node);
1221 this.view.relayout();
1222 this.view.restore_location(node);
1223 },
1224
1225 collapse_node: function (node) {
1226 if (!jm.util.is_node(node)) {
1227 var the_node = this.get_node(node);
1228 if (!the_node) {
1229 logger.error('the node[id=' + node + '] can not be found.');
1230 return;
1231 } else {
1232 return this.collapse_node(the_node);
1233 }
1234 }
1235 if (node.isroot) { return; }
1236 this.view.save_location(node);
1237 this.layout.collapse_node(node);
1238 this.view.relayout();
1239 this.view.restore_location(node);
1240 },
1241
1242 expand_all: function () {
1243 this.layout.expand_all();
1244 this.view.relayout();
1245 },
1246
1247 collapse_all: function () {
1248 this.layout.collapse_all();
1249 this.view.relayout();
1250 },
1251
1252 expand_to_depth: function (depth) {
1253 this.layout.expand_to_depth(depth);
1254 this.view.relayout();
1255 },
1256
1257 _reset: function () {
1258 this.view.reset();
1259 this.layout.reset();
1260 this.data.reset();
1261 },
1262
1263 _show: function (mind) {
1264 var m = mind || jm.format.node_array.example;
1265
1266 this.mind = this.data.load(m);
1267 if (!this.mind) {
1268 logger.error('data.load error');
1269 return;
1270 } else {
1271 logger.debug('data.load ok');
1272 }
1273
1274 this.view.load();
1275 logger.debug('view.load ok');
1276
1277 this.layout.layout();
1278 logger.debug('layout.layout ok');
1279
1280 this.view.show(true);
1281 logger.debug('view.show ok');
1282
1283 this.invoke_event_handle(jm.event_type.show, { data: [mind] });
1284 },
1285
1286 show: function (mind) {
1287 this._reset();
1288 this._show(mind);
1289 },
1290
1291 get_meta: function () {
1292 return {
1293 name: this.mind.name,
1294 author: this.mind.author,
1295 version: this.mind.version
1296 };
1297 },
1298
1299 get_data: function (data_format) {
1300 var df = data_format || 'node_tree';
1301 return this.data.get_data(df);
1302 },
1303
1304 get_root: function () {
1305 return this.mind.root;
1306 },
1307
1308 get_node: function (node) {
1309 if (jm.util.is_node(node)) {
1310 return node;
1311 }
1312 return this.mind.get_node(node);
1313 },
1314
1315 add_node: function (parent_node, nodeid, topic, data, direction) {
1316 if (this.get_editable()) {
1317 var the_parent_node = this.get_node(parent_node);
1318 var dir = jm.direction.of(direction)
1319 if (dir === undefined) {
1320 dir = this.layout.calculate_next_child_direction(the_parent_node);
1321 }
1322 var node = this.mind.add_node(the_parent_node, nodeid, topic, data, dir);
1323 if (!!node) {
1324 this.view.add_node(node);
1325 this.layout.layout();
1326 this.view.show(false);
1327 this.view.reset_node_custom_style(node);
1328 this.expand_node(the_parent_node);
1329 this.invoke_event_handle(jm.event_type.edit, { evt: 'add_node', data: [the_parent_node.id, nodeid, topic, data, dir], node: nodeid });
1330 }
1331 return node;
1332 } else {
1333 logger.error('fail, this mind map is not editable');
1334 return null;
1335 }
1336 },
1337
1338 insert_node_before: function (node_before, nodeid, topic, data, direction) {
1339 if (this.get_editable()) {
1340 var the_node_before = this.get_node(node_before);
1341 var dir = jm.direction.of(direction)
1342 if (dir === undefined) {
1343 dir = this.layout.calculate_next_child_direction(the_node_before.parent);
1344 }
1345 var node = this.mind.insert_node_before(the_node_before, nodeid, topic, data, dir);
1346 if (!!node) {
1347 this.view.add_node(node);
1348 this.layout.layout();
1349 this.view.show(false);
1350 this.invoke_event_handle(jm.event_type.edit, { evt: 'insert_node_before', data: [the_node_before.id, nodeid, topic, data, dir], node: nodeid });
1351 }
1352 return node;
1353 } else {
1354 logger.error('fail, this mind map is not editable');
1355 return null;
1356 }
1357 },
1358
1359 insert_node_after: function (node_after, nodeid, topic, data, direction) {
1360 if (this.get_editable()) {
1361 var the_node_after = this.get_node(node_after);
1362 var dir = jm.direction.of(direction)
1363 if (dir === undefined) {
1364 dir = this.layout.calculate_next_child_direction(the_node_after.parent);
1365 }
1366 var node = this.mind.insert_node_after(the_node_after, nodeid, topic, data, dir);
1367 if (!!node) {
1368 this.view.add_node(node);
1369 this.layout.layout();
1370 this.view.show(false);
1371 this.invoke_event_handle(jm.event_type.edit, { evt: 'insert_node_after', data: [the_node_after.id, nodeid, topic, data, dir], node: nodeid });
1372 }
1373 return node;
1374 } else {
1375 logger.error('fail, this mind map is not editable');
1376 return null;
1377 }
1378 },
1379
1380 remove_node: function (node) {
1381 if (!jm.util.is_node(node)) {
1382 var the_node = this.get_node(node);
1383 if (!the_node) {
1384 logger.error('the node[id=' + node + '] can not be found.');
1385 return false;
1386 } else {
1387 return this.remove_node(the_node);
1388 }
1389 }
1390 if (this.get_editable()) {
1391 if (node.isroot) {
1392 logger.error('fail, can not remove root node');
1393 return false;
1394 }
1395 var nodeid = node.id;
1396 var parentid = node.parent.id;
1397 var parent_node = this.get_node(parentid);
1398 this.view.save_location(parent_node);
1399 this.view.remove_node(node);
1400 this.mind.remove_node(node);
1401 this.layout.layout();
1402 this.view.show(false);
1403 this.view.restore_location(parent_node);
1404 this.invoke_event_handle(jm.event_type.edit, { evt: 'remove_node', data: [nodeid], node: parentid });
1405 return true;
1406 } else {
1407 logger.error('fail, this mind map is not editable');
1408 return false;
1409 }
1410 },
1411
1412 update_node: function (nodeid, topic) {
1413 if (this.get_editable()) {
1414 if (jm.util.text.is_empty(topic)) {
1415 logger.warn('fail, topic can not be empty');
1416 return;
1417 }
1418 var node = this.get_node(nodeid);
1419 if (!!node) {
1420 if (node.topic === topic) {
1421 logger.info('nothing changed');
1422 this.view.update_node(node);
1423 return;
1424 }
1425 node.topic = topic;
1426 this.view.update_node(node);
1427 this.layout.layout();
1428 this.view.show(false);
1429 this.invoke_event_handle(jm.event_type.edit, { evt: 'update_node', data: [nodeid, topic], node: nodeid });
1430 }
1431 } else {
1432 logger.error('fail, this mind map is not editable');
1433 return;
1434 }
1435 },
1436
1437 move_node: function (nodeid, beforeid, parentid, direction) {
1438 if (this.get_editable()) {
1439 var node = this.get_node(nodeid);
1440 var updated_node = this.mind.move_node(node, beforeid, parentid, direction);
1441 if (!!updated_node) {
1442 this.view.update_node(updated_node);
1443 this.layout.layout();
1444 this.view.show(false);
1445 this.invoke_event_handle(jm.event_type.edit, { evt: 'move_node', data: [nodeid, beforeid, parentid, direction], node: nodeid });
1446 }
1447 } else {
1448 logger.error('fail, this mind map is not editable');
1449 return;
1450 }
1451 },
1452
1453 select_node: function (node) {
1454 if (!jm.util.is_node(node)) {
1455 var the_node = this.get_node(node);
1456 if (!the_node) {
1457 logger.error('the node[id=' + node + '] can not be found.');
1458 return;
1459 } else {
1460 return this.select_node(the_node);
1461 }
1462 }
1463 if (!this.layout.is_visible(node)) {
1464 return;
1465 }
1466 this.mind.selected = node;
1467 this.view.select_node(node);
1468 this.invoke_event_handle(jm.event_type.select, { evt: 'select_node', data: [], node: node.id });
1469 },
1470
1471 get_selected_node: function () {
1472 if (!!this.mind) {
1473 return this.mind.selected;
1474 } else {
1475 return null;
1476 }
1477 },
1478
1479 select_clear: function () {
1480 if (!!this.mind) {
1481 this.mind.selected = null;
1482 this.view.select_clear();
1483 }
1484 },
1485
1486 is_node_visible: function (node) {
1487 return this.layout.is_visible(node);
1488 },
1489
1490 find_node_before: function (node) {
1491 if (!jm.util.is_node(node)) {
1492 var the_node = this.get_node(node);
1493 if (!the_node) {
1494 logger.error('the node[id=' + node + '] can not be found.');
1495 return;
1496 } else {
1497 return this.find_node_before(the_node);
1498 }
1499 }
1500 if (node.isroot) { return null; }
1501 var n = null;
1502 if (node.parent.isroot) {
1503 var c = node.parent.children;
1504 var prev = null;
1505 var ni = null;
1506 for (var i = 0; i < c.length; i++) {
1507 ni = c[i];
1508 if (node.direction === ni.direction) {
1509 if (node.id === ni.id) {
1510 n = prev;
1511 }
1512 prev = ni;
1513 }
1514 }
1515 } else {
1516 n = this.mind.get_node_before(node);
1517 }
1518 return n;
1519 },
1520
1521 find_node_after: function (node) {
1522 if (!jm.util.is_node(node)) {
1523 var the_node = this.get_node(node);
1524 if (!the_node) {
1525 logger.error('the node[id=' + node + '] can not be found.');
1526 return;
1527 } else {
1528 return this.find_node_after(the_node);
1529 }
1530 }
1531 if (node.isroot) { return null; }
1532 var n = null;
1533 if (node.parent.isroot) {
1534 var c = node.parent.children;
1535 var getthis = false;
1536 var ni = null;
1537 for (var i = 0; i < c.length; i++) {
1538 ni = c[i];
1539 if (node.direction === ni.direction) {
1540 if (getthis) {
1541 n = ni;
1542 break;
1543 }
1544 if (node.id === ni.id) {
1545 getthis = true;
1546 }
1547 }
1548 }
1549 } else {
1550 n = this.mind.get_node_after(node);
1551 }
1552 return n;
1553 },
1554
1555 set_node_color: function (nodeid, bgcolor, fgcolor) {
1556 if (this.get_editable()) {
1557 var node = this.mind.get_node(nodeid);
1558 if (!!node) {
1559 if (!!bgcolor) {
1560 node.data['background-color'] = bgcolor;
1561 }
1562 if (!!fgcolor) {
1563 node.data['foreground-color'] = fgcolor;
1564 }
1565 this.view.reset_node_custom_style(node);
1566 }
1567 } else {
1568 logger.error('fail, this mind map is not editable');
1569 return null;
1570 }
1571 },
1572
1573 set_node_font_style: function (nodeid, size, weight, style) {
1574 if (this.get_editable()) {
1575 var node = this.mind.get_node(nodeid);
1576 if (!!node) {
1577 if (!!size) {
1578 node.data['font-size'] = size;
1579 }
1580 if (!!weight) {
1581 node.data['font-weight'] = weight;
1582 }
1583 if (!!style) {
1584 node.data['font-style'] = style;
1585 }
1586 this.view.reset_node_custom_style(node);
1587 this.view.update_node(node);
1588 this.layout.layout();
1589 this.view.show(false);
1590 }
1591 } else {
1592 logger.error('fail, this mind map is not editable');
1593 return null;
1594 }
1595 },
1596
1597 set_node_background_image: function (nodeid, image, width, height, rotation) {
1598 if (this.get_editable()) {
1599 var node = this.mind.get_node(nodeid);
1600 if (!!node) {
1601 if (!!image) {
1602 node.data['background-image'] = image;
1603 }
1604 if (!!width) {
1605 node.data['width'] = width;
1606 }
1607 if (!!height) {
1608 node.data['height'] = height;
1609 }
1610 if (!!rotation) {
1611 node.data['background-rotation'] = rotation;
1612 }
1613 this.view.reset_node_custom_style(node);
1614 this.view.update_node(node);
1615 this.layout.layout();
1616 this.view.show(false);
1617 }
1618 } else {
1619 logger.error('fail, this mind map is not editable');
1620 return null;
1621 }
1622 },
1623
1624 set_node_background_rotation: function (nodeid, rotation) {
1625 if (this.get_editable()) {
1626 var node = this.mind.get_node(nodeid);
1627 if (!!node) {
1628 if (!node.data['background-image']) {
1629 logger.error('fail, only can change rotation angle of node with background image');
1630 return null;
1631 }
1632 node.data['background-rotation'] = rotation;
1633 this.view.reset_node_custom_style(node);
1634 this.view.update_node(node);
1635 this.layout.layout();
1636 this.view.show(false);
1637 }
1638 } else {
1639 logger.error('fail, this mind map is not editable');
1640 return null;
1641 }
1642 },
1643
1644 resize: function () {
1645 this.view.resize();
1646 },
1647
1648 // callback(type ,data)
1649 add_event_listener: function (callback) {
1650 if (typeof callback === 'function') {
1651 this.event_handles.push(callback);
1652 }
1653 },
1654
1655 clear_event_listener: function () {
1656 this.event_handles = [];
1657 },
1658
1659 invoke_event_handle: function (type, data) {
1660 var j = this;
1661 $w.setTimeout(function () {
1662 j._invoke_event_handle(type, data);
1663 }, 0);
1664 },
1665
1666 _invoke_event_handle: function (type, data) {
1667 var l = this.event_handles.length;
1668 for (var i = 0; i < l; i++) {
1669 this.event_handles[i](type, data);
1670 }
1671 },
1672
1673 };
1674
1675 // ============= data provider =============================================
1676
1677 jm.data_provider = function (jm) {
1678 this.jm = jm;
1679 };
1680
1681 jm.data_provider.prototype = {
1682 init: function () {
1683 logger.debug('data.init');
1684 },
1685
1686 reset: function () {
1687 logger.debug('data.reset');
1688 },
1689
1690 load: function (mind_data) {
1691 var df = null;
1692 var mind = null;
1693 if (typeof mind_data === 'object') {
1694 if (!!mind_data.format) {
1695 df = mind_data.format;
1696 } else {
1697 df = 'node_tree';
1698 }
1699 } else {
1700 df = 'freemind';
1701 }
1702
1703 if (df == 'node_array') {
1704 mind = jm.format.node_array.get_mind(mind_data);
1705 } else if (df == 'node_tree') {
1706 mind = jm.format.node_tree.get_mind(mind_data);
1707 } else if (df == 'freemind') {
1708 mind = jm.format.freemind.get_mind(mind_data);
1709 } else {
1710 logger.warn('unsupported format');
1711 }
1712 return mind;
1713 },
1714
1715 get_data: function (data_format) {
1716 var data = null;
1717 if (data_format == 'node_array') {
1718 data = jm.format.node_array.get_data(this.jm.mind);
1719 } else if (data_format == 'node_tree') {
1720 data = jm.format.node_tree.get_data(this.jm.mind);
1721 } else if (data_format == 'freemind') {
1722 data = jm.format.freemind.get_data(this.jm.mind);
1723 } else {
1724 logger.error('unsupported ' + data_format + ' format');
1725 }
1726 return data;
1727 },
1728 };
1729
1730 // ============= layout provider ===========================================
1731
1732 jm.layout_provider = function (jm, options) {
1733 this.opts = options;
1734 this.jm = jm;
1735 this.isside = (this.opts.mode == 'side');
1736 this.bounds = null;
1737
1738 this.cache_valid = false;
1739 };
1740
1741 jm.layout_provider.prototype = {
1742 init: function () {
1743 logger.debug('layout.init');
1744 },
1745
1746 reset: function () {
1747 logger.debug('layout.reset');
1748 this.bounds = { n: 0, s: 0, w: 0, e: 0 };
1749 },
1750
1751 calculate_next_child_direction: function (node) {
1752 if (this.isside) {
1753 return jm.direction.right;
1754 }
1755 var children = node.children || [];
1756 var children_len = children.length;
1757 var r = 0;
1758 for (var i = 0; i < children_len; i++) { if (children[i].direction === jm.direction.left) { r--; } else { r++; } }
1759 return (children_len > 1 && r > 0) ? jm.direction.left : jm.direction.right;
1760 },
1761
1762 layout: function () {
1763 logger.debug('layout.layout');
1764 this.layout_direction();
1765 this.layout_offset();
1766 },
1767
1768 layout_direction: function () {
1769 this._layout_direction_root();
1770 },
1771
1772 _layout_direction_root: function () {
1773 var node = this.jm.mind.root;
1774 // logger.debug(node);
1775 var layout_data = null;
1776 if ('layout' in node._data) {
1777 layout_data = node._data.layout;
1778 } else {
1779 layout_data = {};
1780 node._data.layout = layout_data;
1781 }
1782 var children = node.children;
1783 var children_count = children.length;
1784 layout_data.direction = jm.direction.center;
1785 layout_data.side_index = 0;
1786 if (this.isside) {
1787 var i = children_count;
1788 while (i--) {
1789 this._layout_direction_side(children[i], jm.direction.right, i);
1790 }
1791 } else {
1792 var i = children_count;
1793 var subnode = null;
1794 while (i--) {
1795 subnode = children[i];
1796 if (subnode.direction == jm.direction.left) {
1797 this._layout_direction_side(subnode, jm.direction.left, i);
1798 } else {
1799 this._layout_direction_side(subnode, jm.direction.right, i);
1800 }
1801 }
1802 /*
1803 var boundary = Math.ceil(children_count/2);
1804 var i = children_count;
1805 while(i--){
1806 if(i>=boundary){
1807 this._layout_direction_side(children[i],jm.direction.left, children_count-i-1);
1808 }else{
1809 this._layout_direction_side(children[i],jm.direction.right, i);
1810 }
1811 }*/
1812
1813 }
1814 },
1815
1816 _layout_direction_side: function (node, direction, side_index) {
1817 var layout_data = null;
1818 if ('layout' in node._data) {
1819 layout_data = node._data.layout;
1820 } else {
1821 layout_data = {};
1822 node._data.layout = layout_data;
1823 }
1824 var children = node.children;
1825 var children_count = children.length;
1826
1827 layout_data.direction = direction;
1828 layout_data.side_index = side_index;
1829 var i = children_count;
1830 while (i--) {
1831 this._layout_direction_side(children[i], direction, i);
1832 }
1833 },
1834
1835 layout_offset: function () {
1836 var node = this.jm.mind.root;
1837 var layout_data = node._data.layout;
1838 layout_data.offset_x = 0;
1839 layout_data.offset_y = 0;
1840 layout_data.outer_height = 0;
1841 var children = node.children;
1842 var i = children.length;
1843 var left_nodes = [];
1844 var right_nodes = [];
1845 var subnode = null;
1846 while (i--) {
1847 subnode = children[i];
1848 if (subnode._data.layout.direction == jm.direction.right) {
1849 right_nodes.unshift(subnode);
1850 } else {
1851 left_nodes.unshift(subnode);
1852 }
1853 }
1854 layout_data.left_nodes = left_nodes;
1855 layout_data.right_nodes = right_nodes;
1856 layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes);
1857 layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes);
1858 this.bounds.e = node._data.view.width / 2;
1859 this.bounds.w = 0 - this.bounds.e;
1860 //logger.debug(this.bounds.w);
1861 this.bounds.n = 0;
1862 this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right);
1863 },
1864
1865 // layout both the x and y axis
1866 _layout_offset_subnodes: function (nodes) {
1867 var total_height = 0;
1868 var nodes_count = nodes.length;
1869 var i = nodes_count;
1870 var node = null;
1871 var node_outer_height = 0;
1872 var layout_data = null;
1873 var base_y = 0;
1874 var pd = null; // parent._data
1875 while (i--) {
1876 node = nodes[i];
1877 layout_data = node._data.layout;
1878 if (pd == null) {
1879 pd = node.parent._data;
1880 }
1881
1882 node_outer_height = this._layout_offset_subnodes(node.children);
1883 if (!node.expanded) {
1884 node_outer_height = 0;
1885 this.set_visible(node.children, false);
1886 }
1887 node_outer_height = Math.max(node._data.view.height, node_outer_height);
1888 if (node.children.length > 1) {
1889 node_outer_height += this.opts.cousin_space;
1890 }
1891
1892 layout_data.outer_height = node_outer_height;
1893 layout_data.offset_y = base_y - node_outer_height / 2;
1894 layout_data.offset_x = this.opts.hspace * layout_data.direction + pd.view.width * (pd.layout.direction + layout_data.direction) / 2;
1895 if (!node.parent.isroot) {
1896 layout_data.offset_x += this.opts.pspace * layout_data.direction;
1897 }
1898
1899 base_y = base_y - node_outer_height - this.opts.vspace;
1900 total_height += node_outer_height;
1901 }
1902 if (nodes_count > 1) {
1903 total_height += this.opts.vspace * (nodes_count - 1);
1904 }
1905 i = nodes_count;
1906 var middle_height = total_height / 2;
1907 while (i--) {
1908 node = nodes[i];
1909 node._data.layout.offset_y += middle_height;
1910 }
1911 return total_height;
1912 },
1913
1914 // layout the y axis only, for collapse/expand a node
1915 _layout_offset_subnodes_height: function (nodes) {
1916 var total_height = 0;
1917 var nodes_count = nodes.length;
1918 var i = nodes_count;
1919 var node = null;
1920 var node_outer_height = 0;
1921 var layout_data = null;
1922 var base_y = 0;
1923 var pd = null; // parent._data
1924 while (i--) {
1925 node = nodes[i];
1926 layout_data = node._data.layout;
1927 if (pd == null) {
1928 pd = node.parent._data;
1929 }
1930
1931 node_outer_height = this._layout_offset_subnodes_height(node.children);
1932 if (!node.expanded) {
1933 node_outer_height = 0;
1934 }
1935 node_outer_height = Math.max(node._data.view.height, node_outer_height);
1936 if (node.children.length > 1) {
1937 node_outer_height += this.opts.cousin_space;
1938 }
1939
1940 layout_data.outer_height = node_outer_height;
1941 layout_data.offset_y = base_y - node_outer_height / 2;
1942 base_y = base_y - node_outer_height - this.opts.vspace;
1943 total_height += node_outer_height;
1944 }
1945 if (nodes_count > 1) {
1946 total_height += this.opts.vspace * (nodes_count - 1);
1947 }
1948 i = nodes_count;
1949 var middle_height = total_height / 2;
1950 while (i--) {
1951 node = nodes[i];
1952 node._data.layout.offset_y += middle_height;
1953 //logger.debug(node.topic);
1954 //logger.debug(node._data.layout.offset_y);
1955 }
1956 return total_height;
1957 },
1958
1959 get_node_offset: function (node) {
1960 var layout_data = node._data.layout;
1961 var offset_cache = null;
1962 if (('_offset_' in layout_data) && this.cache_valid) {
1963 offset_cache = layout_data._offset_;
1964 } else {
1965 offset_cache = { x: -1, y: -1 };
1966 layout_data._offset_ = offset_cache;
1967 }
1968 if (offset_cache.x == -1 || offset_cache.y == -1) {
1969 var x = layout_data.offset_x;
1970 var y = layout_data.offset_y;
1971 if (!node.isroot) {
1972 var offset_p = this.get_node_offset(node.parent);
1973 x += offset_p.x;
1974 y += offset_p.y;
1975 }
1976 offset_cache.x = x;
1977 offset_cache.y = y;
1978 }
1979 return offset_cache;
1980 },
1981
1982 get_node_point: function (node) {
1983 var view_data = node._data.view;
1984 var offset_p = this.get_node_offset(node);
1985 //logger.debug(offset_p);
1986 var p = {};
1987 p.x = offset_p.x + view_data.width * (node._data.layout.direction - 1) / 2;
1988 p.y = offset_p.y - view_data.height / 2;
1989 //logger.debug(p);
1990 return p;
1991 },
1992
1993 get_node_point_in: function (node) {
1994 var p = this.get_node_offset(node);
1995 return p;
1996 },
1997
1998 get_node_point_out: function (node) {
1999 var layout_data = node._data.layout;
2000 var pout_cache = null;
2001 if (('_pout_' in layout_data) && this.cache_valid) {
2002 pout_cache = layout_data._pout_;
2003 } else {
2004 pout_cache = { x: -1, y: -1 };
2005 layout_data._pout_ = pout_cache;
2006 }
2007 if (pout_cache.x == -1 || pout_cache.y == -1) {
2008 if (node.isroot) {
2009 pout_cache.x = 0;
2010 pout_cache.y = 0;
2011 } else {
2012 var view_data = node._data.view;
2013 var offset_p = this.get_node_offset(node);
2014 pout_cache.x = offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction;
2015 pout_cache.y = offset_p.y;
2016 //logger.debug('pout');
2017 //logger.debug(pout_cache);
2018 }
2019 }
2020 return pout_cache;
2021 },
2022
2023 get_expander_point: function (node) {
2024 var p = this.get_node_point_out(node);
2025 var ex_p = {};
2026 if (node._data.layout.direction == jm.direction.right) {
2027 ex_p.x = p.x - this.opts.pspace;
2028 } else {
2029 ex_p.x = p.x;
2030 }
2031 ex_p.y = p.y - Math.ceil(this.opts.pspace / 2);
2032 return ex_p;
2033 },
2034
2035 get_min_size: function () {
2036 var nodes = this.jm.mind.nodes;
2037 var node = null;
2038 var pout = null;
2039 for (var nodeid in nodes) {
2040 node = nodes[nodeid];
2041 pout = this.get_node_point_out(node);
2042 if (pout.x > this.bounds.e) { this.bounds.e = pout.x; }
2043 if (pout.x < this.bounds.w) { this.bounds.w = pout.x; }
2044 }
2045 return {
2046 w: this.bounds.e - this.bounds.w,
2047 h: this.bounds.s - this.bounds.n
2048 }
2049 },
2050
2051 toggle_node: function (node) {
2052 if (node.isroot) {
2053 return;
2054 }
2055 if (node.expanded) {
2056 this.collapse_node(node);
2057 } else {
2058 this.expand_node(node);
2059 }
2060 },
2061
2062 expand_node: function (node) {
2063 node.expanded = true;
2064 this.part_layout(node);
2065 this.set_visible(node.children, true);
2066 this.jm.invoke_event_handle(jm.event_type.show, { evt: 'expand_node', data: [], node: node.id });
2067 },
2068
2069 collapse_node: function (node) {
2070 node.expanded = false;
2071 this.part_layout(node);
2072 this.set_visible(node.children, false);
2073 this.jm.invoke_event_handle(jm.event_type.show, { evt: 'collapse_node', data: [], node: node.id });
2074 },
2075
2076 expand_all: function () {
2077 var nodes = this.jm.mind.nodes;
2078 var c = 0;
2079 var node;
2080 for (var nodeid in nodes) {
2081 node = nodes[nodeid];
2082 if (!node.expanded) {
2083 node.expanded = true;
2084 c++;
2085 }
2086 }
2087 if (c > 0) {
2088 var root = this.jm.mind.root;
2089 this.part_layout(root);
2090 this.set_visible(root.children, true);
2091 }
2092 },
2093
2094 collapse_all: function () {
2095 var nodes = this.jm.mind.nodes;
2096 var c = 0;
2097 var node;
2098 for (var nodeid in nodes) {
2099 node = nodes[nodeid];
2100 if (node.expanded && !node.isroot) {
2101 node.expanded = false;
2102 c++;
2103 }
2104 }
2105 if (c > 0) {
2106 var root = this.jm.mind.root;
2107 this.part_layout(root);
2108 this.set_visible(root.children, true);
2109 }
2110 },
2111
2112 expand_to_depth: function (target_depth, curr_nodes, curr_depth) {
2113 if (target_depth < 1) { return; }
2114 var nodes = curr_nodes || this.jm.mind.root.children;
2115 var depth = curr_depth || 1;
2116 var i = nodes.length;
2117 var node = null;
2118 while (i--) {
2119 node = nodes[i];
2120 if (depth < target_depth) {
2121 if (!node.expanded) {
2122 this.expand_node(node);
2123 }
2124 this.expand_to_depth(target_depth, node.children, depth + 1);
2125 }
2126 if (depth == target_depth) {
2127 if (node.expanded) {
2128 this.collapse_node(node);
2129 }
2130 }
2131 }
2132 },
2133
2134 part_layout: function (node) {
2135 var root = this.jm.mind.root;
2136 if (!!root) {
2137 var root_layout_data = root._data.layout;
2138 if (node.isroot) {
2139 root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes);
2140 root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes);
2141 } else {
2142 if (node._data.layout.direction == jm.direction.right) {
2143 root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes);
2144 } else {
2145 root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes);
2146 }
2147 }
2148 this.bounds.s = Math.max(root_layout_data.outer_height_left, root_layout_data.outer_height_right);
2149 this.cache_valid = false;
2150 } else {
2151 logger.warn('can not found root node');
2152 }
2153 },
2154
2155 set_visible: function (nodes, visible) {
2156 var i = nodes.length;
2157 var node = null;
2158 var layout_data = null;
2159 while (i--) {
2160 node = nodes[i];
2161 layout_data = node._data.layout;
2162 if (node.expanded) {
2163 this.set_visible(node.children, visible);
2164 } else {
2165 this.set_visible(node.children, false);
2166 }
2167 if (!node.isroot) {
2168 node._data.layout.visible = visible;
2169 }
2170 }
2171 },
2172
2173 is_expand: function (node) {
2174 return node.expanded;
2175 },
2176
2177 is_visible: function (node) {
2178 var layout_data = node._data.layout;
2179 if (('visible' in layout_data) && !layout_data.visible) {
2180 return false;
2181 } else {
2182 return true;
2183 }
2184 }
2185 };
2186
2187 jm.graph_canvas = function (view) {
2188 this.opts = view.opts;
2189 this.e_canvas = $c('canvas');
2190 this.e_canvas.className = 'jsmind';
2191 this.canvas_ctx = this.e_canvas.getContext('2d');
2192 this.size = { w: 0, h: 0 };
2193 };
2194
2195 jm.graph_canvas.prototype = {
2196 element: function () {
2197 return this.e_canvas;
2198 },
2199
2200 set_size: function (w, h) {
2201 this.size.w = w;
2202 this.size.h = h;
2203 this.e_canvas.width = w;
2204 this.e_canvas.height = h;
2205 },
2206
2207 clear: function () {
2208 this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h);
2209 },
2210
2211 draw_line: function (pout, pin, offset) {
2212 var ctx = this.canvas_ctx;
2213 ctx.strokeStyle = this.opts.line_color;
2214 ctx.lineWidth = this.opts.line_width;
2215 ctx.lineCap = 'round';
2216
2217 this._bezier_to(ctx,
2218 pin.x + offset.x,
2219 pin.y + offset.y,
2220 pout.x + offset.x,
2221 pout.y + offset.y);
2222 },
2223
2224 copy_to: function (dest_canvas_ctx, callback) {
2225 dest_canvas_ctx.drawImage(this.e_canvas, 0, 0);
2226 !!callback && callback();
2227 },
2228
2229 _bezier_to: function (ctx, x1, y1, x2, y2) {
2230 ctx.beginPath();
2231 ctx.moveTo(x1, y1);
2232 ctx.bezierCurveTo(x1 + (x2 - x1) * 2 / 3, y1, x1, y2, x2, y2);
2233 ctx.stroke();
2234 },
2235
2236 _line_to: function (ctx, x1, y1, x2, y2) {
2237 ctx.beginPath();
2238 ctx.moveTo(x1, y1);
2239 ctx.lineTo(x2, y2);
2240 ctx.stroke();
2241 }
2242 };
2243
2244 jm.graph_svg = function (view) {
2245 this.view = view;
2246 this.opts = view.opts;
2247 this.e_svg = jm.graph_svg.c('svg');
2248 this.e_svg.setAttribute('class', 'jsmind');
2249 this.size = { w: 0, h: 0 };
2250 this.lines = [];
2251 };
2252
2253 jm.graph_svg.c = function (tag) {
2254 return $d.createElementNS('http://www.w3.org/2000/svg', tag);
2255 };
2256
2257 jm.graph_svg.prototype = {
2258 element: function () {
2259 return this.e_svg;
2260 },
2261
2262 set_size: function (w, h) {
2263 this.size.w = w;
2264 this.size.h = h;
2265 this.e_svg.setAttribute('width', w);
2266 this.e_svg.setAttribute('height', h);
2267 },
2268
2269 clear: function () {
2270 var len = this.lines.length;
2271 while (len--) {
2272 this.e_svg.removeChild(this.lines[len]);
2273 }
2274 this.lines.length = 0;
2275 },
2276
2277 draw_line: function (pout, pin, offset) {
2278 var line = jm.graph_svg.c('path');
2279 line.setAttribute('stroke', this.opts.line_color);
2280 line.setAttribute('stroke-width', this.opts.line_width);
2281 line.setAttribute('fill', 'transparent');
2282 this.lines.push(line);
2283 this.e_svg.appendChild(line);
2284 this._bezier_to(line, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y);
2285 },
2286
2287 copy_to: function (dest_canvas_ctx, callback) {
2288 var img = new Image();
2289 img.onload = function () {
2290 dest_canvas_ctx.drawImage(img, 0, 0);
2291 !!callback && callback();
2292 }
2293 img.src = 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg));
2294 },
2295
2296 _bezier_to: function (path, x1, y1, x2, y2) {
2297 path.setAttribute('d', 'M' + x1 + ' ' + y1 + ' C ' + (x1 + (x2 - x1) * 2 / 3) + ' ' + y1 + ', ' + x1 + ' ' + y2 + ', ' + x2 + ' ' + y2);
2298 },
2299
2300 _line_to: function (path, x1, y1, x2, y2) {
2301 path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2);
2302 }
2303 };
2304
2305 // view provider
2306 jm.view_provider = function (jm, options) {
2307 this.opts = options;
2308 this.jm = jm;
2309 this.layout = jm.layout;
2310
2311 this.container = null;
2312 this.e_panel = null;
2313 this.e_nodes = null;
2314
2315 this.size = { w: 0, h: 0 };
2316
2317 this.selected_node = null;
2318 this.editing_node = null;
2319
2320 this.graph = null;
2321 };
2322
2323 jm.view_provider.prototype = {
2324 init: function () {
2325 logger.debug('view.init');
2326
2327 this.container = $i(this.opts.container) ? this.opts.container : $g(this.opts.container);
2328 if (!this.container) {
2329 logger.error('the options.view.container was not be found in dom');
2330 return;
2331 }
2332 this.e_panel = $c('div');
2333 this.e_nodes = $c('jmnodes');
2334 this.e_editor = $c('input');
2335
2336 this.graph = this.opts.engine.toLowerCase() === 'svg' ? new jm.graph_svg(this) : new jm.graph_canvas(this);
2337 this.e_panel.className = 'jsmind-inner jmnode-overflow-' + this.opts.node_overflow;
2338 this.e_panel.tabIndex = 1;
2339 this.e_panel.appendChild(this.graph.element());
2340 this.e_panel.appendChild(this.e_nodes);
2341
2342 this.e_editor.className = 'jsmind-editor';
2343 this.e_editor.type = 'text';
2344
2345 this.actualZoom = 1;
2346 this.zoomStep = 0.1;
2347 this.minZoom = 0.5;
2348 this.maxZoom = 2;
2349
2350 var v = this;
2351 jm.util.dom.add_event(this.e_editor, 'keydown', function (e) {
2352 var evt = e || event;
2353 if (evt.keyCode == 13) { v.edit_node_end(); evt.stopPropagation(); }
2354 });
2355 jm.util.dom.add_event(this.e_editor, 'blur', function (e) {
2356 v.edit_node_end();
2357 });
2358
2359 this.container.appendChild(this.e_panel);
2360
2361 // Used to avoid dragging, while editing node.
2362 this.dragging_enabled = true
2363
2364 this.draggable_canvas()
2365 },
2366
2367 add_event: function (obj, event_name, event_handle) {
2368 jm.util.dom.add_event(this.e_nodes, event_name, function (e) {
2369 var evt = e || event;
2370 event_handle.call(obj, evt);
2371 });
2372 },
2373
2374 get_binded_nodeid: function (element) {
2375 if (element == null) {
2376 return null;
2377 }
2378 var tagName = element.tagName.toLowerCase();
2379 if (tagName == 'jmnode' || tagName == 'jmexpander') {
2380 return element.getAttribute('nodeid');
2381 } else if (tagName == 'jmnodes' || tagName == 'body' || tagName == 'html') {
2382 return null;
2383 } else {
2384 return this.get_binded_nodeid(element.parentElement);
2385 }
2386 },
2387
2388 is_node: function (element) {
2389 if (element == null) {
2390 return false;
2391 }
2392 var tagName = element.tagName.toLowerCase();
2393 if (tagName == 'jmnode') {
2394 return true;
2395 } else if (tagName == 'jmnodes' || tagName == 'body' || tagName == 'html') {
2396 return false;
2397 } else {
2398 return this.is_node(element.parentElement);
2399 }
2400 },
2401
2402 is_expander: function (element) {
2403 return (element.tagName.toLowerCase() == 'jmexpander');
2404 },
2405
2406 reset: function () {
2407 logger.debug('view.reset');
2408 this.selected_node = null;
2409 this.clear_lines();
2410 this.clear_nodes();
2411 this.reset_theme();
2412 },
2413
2414 reset_theme: function () {
2415 var theme_name = this.jm.options.theme;
2416 if (!!theme_name) {
2417 this.e_nodes.className = 'theme-' + theme_name;
2418 } else {
2419 this.e_nodes.className = '';
2420 }
2421 },
2422
2423 reset_custom_style: function () {
2424 var nodes = this.jm.mind.nodes;
2425 for (var nodeid in nodes) {
2426 this.reset_node_custom_style(nodes[nodeid]);
2427 }
2428 },
2429
2430 load: function () {
2431 logger.debug('view.load');
2432 this.init_nodes();
2433 },
2434
2435 expand_size: function () {
2436 var min_size = this.layout.get_min_size();
2437 var min_width = min_size.w + this.opts.hmargin * 2;
2438 var min_height = min_size.h + this.opts.vmargin * 2;
2439 var client_w = this.e_panel.clientWidth;
2440 var client_h = this.e_panel.clientHeight;
2441 if (client_w < min_width) { client_w = min_width; }
2442 if (client_h < min_height) { client_h = min_height; }
2443 this.size.w = client_w;
2444 this.size.h = client_h;
2445 },
2446
2447 init_nodes_size: function (node) {
2448 var view_data = node._data.view;
2449 view_data.width = view_data.element.clientWidth;
2450 view_data.height = view_data.element.clientHeight;
2451 },
2452
2453 init_nodes: function () {
2454 var nodes = this.jm.mind.nodes;
2455 var doc_frag = $d.createDocumentFragment();
2456 for (var nodeid in nodes) {
2457 this.create_node_element(nodes[nodeid], doc_frag);
2458 }
2459 this.e_nodes.appendChild(doc_frag);
2460 for (var nodeid in nodes) {
2461 this.init_nodes_size(nodes[nodeid]);
2462 }
2463 },
2464
2465 add_node: function (node) {
2466 this.create_node_element(node, this.e_nodes);
2467 this.init_nodes_size(node);
2468 },
2469
2470 create_node_element: function (node, parent_node) {
2471 var view_data = null;
2472 if ('view' in node._data) {
2473 view_data = node._data.view;
2474 } else {
2475 view_data = {};
2476 node._data.view = view_data;
2477 }
2478
2479 var d = $c('jmnode');
2480 if (node.isroot) {
2481 d.className = 'root';
2482 } else {
2483 var d_e = $c('jmexpander');
2484 $t(d_e, '-');
2485 d_e.setAttribute('nodeid', node.id);
2486 d_e.style.visibility = 'hidden';
2487 parent_node.appendChild(d_e);
2488 view_data.expander = d_e;
2489 }
2490 if (!!node.topic) {
2491 if (this.opts.support_html) {
2492 $h(d, node.topic);
2493 } else {
2494 $t(d, node.topic);
2495 }
2496 }
2497 d.setAttribute('nodeid', node.id);
2498 d.style.visibility = 'hidden';
2499 this._reset_node_custom_style(d, node.data);
2500
2501 parent_node.appendChild(d);
2502 view_data.element = d;
2503 },
2504
2505 remove_node: function (node) {
2506 if (this.selected_node != null && this.selected_node.id == node.id) {
2507 this.selected_node = null;
2508 }
2509 if (this.editing_node != null && this.editing_node.id == node.id) {
2510 node._data.view.element.removeChild(this.e_editor);
2511 this.editing_node = null;
2512 }
2513 var children = node.children;
2514 var i = children.length;
2515 while (i--) {
2516 this.remove_node(children[i]);
2517 }
2518 if (node._data.view) {
2519 var element = node._data.view.element;
2520 var expander = node._data.view.expander;
2521 this.e_nodes.removeChild(element);
2522 this.e_nodes.removeChild(expander);
2523 node._data.view.element = null;
2524 node._data.view.expander = null;
2525 }
2526 },
2527
2528 update_node: function (node) {
2529 var view_data = node._data.view;
2530 var element = view_data.element;
2531 if (!!node.topic) {
2532 if (this.opts.support_html) {
2533 $h(element, node.topic);
2534 } else {
2535 $t(element, node.topic);
2536 }
2537 }
2538 if (this.layout.is_visible(node)) {
2539 view_data.width = element.clientWidth;
2540 view_data.height = element.clientHeight;
2541 } else {
2542 let origin_style = element.getAttribute('style');
2543 element.style = 'visibility: visible; left:0; top:0;';
2544 view_data.width = element.clientWidth;
2545 view_data.height = element.clientHeight;
2546 element.style = origin_style;
2547 }
2548 },
2549
2550 select_node: function (node) {
2551 if (!!this.selected_node) {
2552 this.selected_node._data.view.element.className =
2553 this.selected_node._data.view.element.className.replace(/\s*selected\b/i, '');
2554 this.reset_node_custom_style(this.selected_node);
2555 }
2556 if (!!node) {
2557 this.selected_node = node;
2558 node._data.view.element.className += ' selected';
2559 this.clear_node_custom_style(node);
2560 }
2561 },
2562
2563 select_clear: function () {
2564 this.select_node(null);
2565 },
2566
2567 get_editing_node: function () {
2568 return this.editing_node;
2569 },
2570
2571 is_editing: function () {
2572 return (!!this.editing_node);
2573 },
2574
2575 edit_node_begin: function (node) {
2576 if (!node.topic) {
2577 logger.warn("don't edit image nodes");
2578 return;
2579 }
2580 if (this.editing_node != null) {
2581 this.edit_node_end();
2582 }
2583 this.editing_node = node;
2584 var view_data = node._data.view;
2585 var element = view_data.element;
2586 var topic = node.topic;
2587 var ncs = getComputedStyle(element);
2588 this.e_editor.value = topic;
2589 this.e_editor.style.width = (element.clientWidth - parseInt(ncs.getPropertyValue('padding-left')) - parseInt(ncs.getPropertyValue('padding-right'))) + 'px';
2590 element.innerHTML = '';
2591 element.appendChild(this.e_editor);
2592 element.style.zIndex = 5;
2593 this.e_editor.focus();
2594 this.e_editor.select();
2595 },
2596
2597 edit_node_end: function () {
2598 if (this.editing_node != null) {
2599 var node = this.editing_node;
2600 this.editing_node = null;
2601 var view_data = node._data.view;
2602 var element = view_data.element;
2603 var topic = this.e_editor.value;
2604 element.style.zIndex = 'auto';
2605 element.removeChild(this.e_editor);
2606 if (jm.util.text.is_empty(topic) || node.topic === topic) {
2607 if (this.opts.support_html) {
2608 $h(element, node.topic);
2609 } else {
2610 $t(element, node.topic);
2611 }
2612 } else {
2613 this.jm.update_node(node.id, topic);
2614 }
2615 }
2616 this.e_panel.focus();
2617 },
2618
2619 get_view_offset: function () {
2620 var bounds = this.layout.bounds;
2621 var _x = (this.size.w - bounds.e - bounds.w) / 2;
2622 var _y = this.size.h / 2;
2623 return { x: _x, y: _y };
2624 },
2625
2626 resize: function () {
2627 this.graph.set_size(1, 1);
2628 this.e_nodes.style.width = '1px';
2629 this.e_nodes.style.height = '1px';
2630
2631 this.expand_size();
2632 this._show();
2633 },
2634
2635 _show: function () {
2636 this.graph.set_size(this.size.w, this.size.h);
2637 this.e_nodes.style.width = this.size.w + 'px';
2638 this.e_nodes.style.height = this.size.h + 'px';
2639 this.show_nodes();
2640 this.show_lines();
2641 //this.layout.cache_valid = true;
2642 this.jm.invoke_event_handle(jm.event_type.resize, { data: [] });
2643 },
2644
2645 zoomIn: function () {
2646 return this.setZoom(this.actualZoom + this.zoomStep);
2647 },
2648
2649 zoomOut: function () {
2650 return this.setZoom(this.actualZoom - this.zoomStep);
2651 },
2652
2653 setZoom: function (zoom) {
2654 if ((zoom < this.minZoom) || (zoom > this.maxZoom)) {
2655 return false;
2656 }
2657 this.actualZoom = zoom;
2658 for (var i = 0; i < this.e_panel.children.length; i++) {
2659 this.e_panel.children[i].style.zoom = zoom;
2660 };
2661 this.show(true);
2662 return true;
2663
2664 },
2665
2666 _center_root: function () {
2667 // center root node
2668 var outer_w = this.e_panel.clientWidth;
2669 var outer_h = this.e_panel.clientHeight;
2670 if (this.size.w > outer_w) {
2671 var _offset = this.get_view_offset();
2672 this.e_panel.scrollLeft = _offset.x * this.actualZoom - outer_w / 2;
2673 }
2674 if (this.size.h > outer_h) {
2675 this.e_panel.scrollTop = (this.size.h * this.actualZoom - outer_h) / 2;
2676 }
2677 },
2678
2679 show: function (keep_center) {
2680 logger.debug('view.show');
2681 this.expand_size();
2682 this._show();
2683 if (!!keep_center) {
2684 this._center_root();
2685 }
2686 },
2687
2688 relayout: function () {
2689 this.expand_size();
2690 this._show();
2691 },
2692
2693 save_location: function (node) {
2694 var vd = node._data.view;
2695 vd._saved_location = {
2696 x: parseInt(vd.element.style.left) - this.e_panel.scrollLeft,
2697 y: parseInt(vd.element.style.top) - this.e_panel.scrollTop,
2698 };
2699 },
2700
2701 restore_location: function (node) {
2702 var vd = node._data.view;
2703 this.e_panel.scrollLeft = parseInt(vd.element.style.left) - vd._saved_location.x;
2704 this.e_panel.scrollTop = parseInt(vd.element.style.top) - vd._saved_location.y;
2705 },
2706
2707 clear_nodes: function () {
2708 var mind = this.jm.mind;
2709 if (mind == null) {
2710 return;
2711 }
2712 var nodes = mind.nodes;
2713 var node = null;
2714 for (var nodeid in nodes) {
2715 node = nodes[nodeid];
2716 node._data.view.element = null;
2717 node._data.view.expander = null;
2718 }
2719 this.e_nodes.innerHTML = '';
2720 },
2721
2722 show_nodes: function () {
2723 var nodes = this.jm.mind.nodes;
2724 var node = null;
2725 var node_element = null;
2726 var expander = null;
2727 var p = null;
2728 var p_expander = null;
2729 var expander_text = '-';
2730 var view_data = null;
2731 var _offset = this.get_view_offset();
2732 for (var nodeid in nodes) {
2733 node = nodes[nodeid];
2734 view_data = node._data.view;
2735 node_element = view_data.element;
2736 expander = view_data.expander;
2737 if (!this.layout.is_visible(node)) {
2738 node_element.style.display = 'none';
2739 expander.style.display = 'none';
2740 continue;
2741 }
2742 this.reset_node_custom_style(node);
2743 p = this.layout.get_node_point(node);
2744 view_data.abs_x = _offset.x + p.x;
2745 view_data.abs_y = _offset.y + p.y;
2746 node_element.style.left = (_offset.x + p.x) + 'px';
2747 node_element.style.top = (_offset.y + p.y) + 'px';
2748 node_element.style.display = '';
2749 node_element.style.visibility = 'visible';
2750 if (!node.isroot && node.children.length > 0) {
2751 expander_text = node.expanded ? '-' : '+';
2752 p_expander = this.layout.get_expander_point(node);
2753 expander.style.left = (_offset.x + p_expander.x) + 'px';
2754 expander.style.top = (_offset.y + p_expander.y) + 'px';
2755 expander.style.display = '';
2756 expander.style.visibility = 'visible';
2757 $t(expander, expander_text);
2758 }
2759 // hide expander while all children have been removed
2760 if (!node.isroot && node.children.length == 0) {
2761 expander.style.display = 'none';
2762 expander.style.visibility = 'hidden';
2763 }
2764 }
2765 },
2766
2767 reset_node_custom_style: function (node) {
2768 this._reset_node_custom_style(node._data.view.element, node.data);
2769 },
2770
2771 _reset_node_custom_style: function (node_element, node_data) {
2772 if ('background-color' in node_data) {
2773 node_element.style.backgroundColor = node_data['background-color'];
2774 }
2775 if ('foreground-color' in node_data) {
2776 node_element.style.color = node_data['foreground-color'];
2777 }
2778 if ('width' in node_data) {
2779 node_element.style.width = node_data['width'] + 'px';
2780 }
2781 if ('height' in node_data) {
2782 node_element.style.height = node_data['height'] + 'px';
2783 }
2784 if ('font-size' in node_data) {
2785 node_element.style.fontSize = node_data['font-size'] + 'px';
2786 }
2787 if ('font-weight' in node_data) {
2788 node_element.style.fontWeight = node_data['font-weight'];
2789 }
2790 if ('font-style' in node_data) {
2791 node_element.style.fontStyle = node_data['font-style'];
2792 }
2793 if ('background-image' in node_data) {
2794 var backgroundImage = node_data['background-image'];
2795 if (backgroundImage.startsWith('data') && node_data['width'] && node_data['height']) {
2796 var img = new Image();
2797
2798 img.onload = function () {
2799 var c = $c('canvas');
2800 c.width = node_element.clientWidth;
2801 c.height = node_element.clientHeight;
2802 var img = this;
2803 if (c.getContext) {
2804 var ctx = c.getContext('2d');
2805 ctx.drawImage(img, 2, 2, node_element.clientWidth, node_element.clientHeight);
2806 var scaledImageData = c.toDataURL();
2807 node_element.style.backgroundImage = 'url(' + scaledImageData + ')';
2808 }
2809 };
2810 img.src = backgroundImage;
2811
2812 } else {
2813 node_element.style.backgroundImage = 'url(' + backgroundImage + ')';
2814 }
2815 node_element.style.backgroundSize = '99%';
2816
2817 if ('background-rotation' in node_data) {
2818 node_element.style.transform = 'rotate(' + node_data['background-rotation'] + 'deg)';
2819 }
2820 }
2821 },
2822
2823 clear_node_custom_style: function (node) {
2824 var node_element = node._data.view.element;
2825 node_element.style.backgroundColor = "";
2826 node_element.style.color = "";
2827 },
2828
2829 clear_lines: function () {
2830 this.graph.clear();
2831 },
2832
2833 show_lines: function () {
2834 this.clear_lines();
2835 var nodes = this.jm.mind.nodes;
2836 var node = null;
2837 var pin = null;
2838 var pout = null;
2839 var _offset = this.get_view_offset();
2840 for (var nodeid in nodes) {
2841 node = nodes[nodeid];
2842 if (!!node.isroot) { continue; }
2843 if (('visible' in node._data.layout) && !node._data.layout.visible) { continue; }
2844 pin = this.layout.get_node_point_in(node);
2845 pout = this.layout.get_node_point_out(node.parent);
2846 this.graph.draw_line(pout, pin, _offset);
2847 }
2848 },
2849
2850 // Drag the whole mind map with your mouse (usefull when it's larger that the container).
2851 draggable_canvas: function () {
2852 // If draggable option is true.
2853 if (this.opts.draggable) {
2854 // Dragging disabled by default.
2855 let dragging = false
2856 let x, y
2857 if (this.opts.hide_scrollbars_when_draggable) {
2858 // Avoid scrollbars when mind map is larger than the container (e_panel = id jsmind-inner)
2859 this.e_panel.style = 'overflow: hidden'
2860 }
2861 // Move the whole mind map with mouse moves, while button is down.
2862 jm.util.dom.add_event(this.container, 'mousedown', (eventDown) => {
2863 dragging = true
2864 // Record current mouse position.
2865 x = eventDown.clientX
2866 y = eventDown.clientY
2867 })
2868 // Stop moving mind map once mouse button is released.
2869 jm.util.dom.add_event(this.container, 'mouseup', () => {
2870 dragging = false
2871 })
2872 // Follow current mouse position and move mind map accordingly.
2873 jm.util.dom.add_event(this.container, 'mousemove', (eventMove) => {
2874 if (this.dragging_enabled && dragging) {
2875 this.e_panel.scrollBy(x - eventMove.clientX, y - eventMove.clientY)
2876 // Record new current position.
2877 x = eventMove.clientX
2878 y = eventMove.clientY
2879 }
2880 })
2881 }
2882 },
2883
2884 get_draggable_canvas: function () {
2885 return this.opts.draggable
2886 },
2887
2888 enable_draggable_canvas: function () {
2889 this.dragging_enabled = true
2890 },
2891
2892 disable_draggable_canvas: function () {
2893 this.dragging_enabled = false
2894 },
2895
2896 };
2897
2898 // shortcut provider
2899 jm.shortcut_provider = function (jm, options) {
2900 this.jm = jm;
2901 this.opts = options;
2902 this.mapping = options.mapping;
2903 this.handles = options.handles;
2904 this._newid = null;
2905 this._mapping = {};
2906 };
2907
2908 jm.shortcut_provider.prototype = {
2909 init: function () {
2910 jm.util.dom.add_event(this.jm.view.e_panel, 'keydown', this.handler.bind(this));
2911
2912 this.handles['addchild'] = this.handle_addchild;
2913 this.handles['addbrother'] = this.handle_addbrother;
2914 this.handles['editnode'] = this.handle_editnode;
2915 this.handles['delnode'] = this.handle_delnode;
2916 this.handles['toggle'] = this.handle_toggle;
2917 this.handles['up'] = this.handle_up;
2918 this.handles['down'] = this.handle_down;
2919 this.handles['left'] = this.handle_left;
2920 this.handles['right'] = this.handle_right;
2921
2922 for (var handle in this.mapping) {
2923 if (!!this.mapping[handle] && (handle in this.handles)) {
2924 var keys = this.mapping[handle];
2925 if(!Array.isArray(keys)){
2926 keys = [keys]
2927 }
2928 for(let key of keys){
2929 this._mapping[key] = this.handles[handle];
2930 }
2931 }
2932 }
2933
2934 if (typeof this.opts.id_generator === 'function') {
2935 this._newid = this.opts.id_generator;
2936 } else {
2937 this._newid = jm.util.uuid.newid;
2938 }
2939 },
2940
2941 enable_shortcut: function () {
2942 this.opts.enable = true;
2943 },
2944
2945 disable_shortcut: function () {
2946 this.opts.enable = false;
2947 },
2948
2949 handler: function (e) {
2950 if (e.which == 9) { e.preventDefault(); } //prevent tab to change focus in browser
2951 if (this.jm.view.is_editing()) { return; }
2952 var evt = e || event;
2953 if (!this.opts.enable) { return true; }
2954 var kc = evt.keyCode + (evt.metaKey << 13) + (evt.ctrlKey << 12) + (evt.altKey << 11) + (evt.shiftKey << 10);
2955 if (kc in this._mapping) {
2956 this._mapping[kc].call(this, this.jm, e);
2957 }
2958 },
2959
2960 handle_addchild: function (_jm, e) {
2961 var selected_node = _jm.get_selected_node();
2962 if (!!selected_node) {
2963 var nodeid = this._newid();
2964 var node = _jm.add_node(selected_node, nodeid, 'New Node');
2965 if (!!node) {
2966 _jm.select_node(nodeid);
2967 _jm.begin_edit(nodeid);
2968 }
2969 }
2970 },
2971 handle_addbrother: function (_jm, e) {
2972 var selected_node = _jm.get_selected_node();
2973 if (!!selected_node && !selected_node.isroot) {
2974 var nodeid = this._newid();
2975 var node = _jm.insert_node_after(selected_node, nodeid, 'New Node');
2976 if (!!node) {
2977 _jm.select_node(nodeid);
2978 _jm.begin_edit(nodeid);
2979 }
2980 }
2981 },
2982 handle_editnode: function (_jm, e) {
2983 var selected_node = _jm.get_selected_node();
2984 if (!!selected_node) {
2985 _jm.begin_edit(selected_node);
2986 }
2987 },
2988 handle_delnode: function (_jm, e) {
2989 var selected_node = _jm.get_selected_node();
2990 if (!!selected_node && !selected_node.isroot) {
2991 _jm.select_node(selected_node.parent);
2992 _jm.remove_node(selected_node);
2993 }
2994 },
2995 handle_toggle: function (_jm, e) {
2996 var evt = e || event;
2997 var selected_node = _jm.get_selected_node();
2998 if (!!selected_node) {
2999 _jm.toggle_node(selected_node.id);
3000 evt.stopPropagation();
3001 evt.preventDefault();
3002 }
3003 },
3004 handle_up: function (_jm, e) {
3005 var evt = e || event;
3006 var selected_node = _jm.get_selected_node();
3007 if (!!selected_node) {
3008 var up_node = _jm.find_node_before(selected_node);
3009 if (!up_node) {
3010 var np = _jm.find_node_before(selected_node.parent);
3011 if (!!np && np.children.length > 0) {
3012 up_node = np.children[np.children.length - 1];
3013 }
3014 }
3015 if (!!up_node) {
3016 _jm.select_node(up_node);
3017 }
3018 evt.stopPropagation();
3019 evt.preventDefault();
3020 }
3021 },
3022
3023 handle_down: function (_jm, e) {
3024 var evt = e || event;
3025 var selected_node = _jm.get_selected_node();
3026 if (!!selected_node) {
3027 var down_node = _jm.find_node_after(selected_node);
3028 if (!down_node) {
3029 var np = _jm.find_node_after(selected_node.parent);
3030 if (!!np && np.children.length > 0) {
3031 down_node = np.children[0];
3032 }
3033 }
3034 if (!!down_node) {
3035 _jm.select_node(down_node);
3036 }
3037 evt.stopPropagation();
3038 evt.preventDefault();
3039 }
3040 },
3041
3042 handle_left: function (_jm, e) {
3043 this._handle_direction(_jm, e, jm.direction.left);
3044 },
3045 handle_right: function (_jm, e) {
3046 this._handle_direction(_jm, e, jm.direction.right);
3047 },
3048 _handle_direction: function (_jm, e, d) {
3049 var evt = e || event;
3050 var selected_node = _jm.get_selected_node();
3051 var node = null;
3052 if (!!selected_node) {
3053 if (selected_node.isroot) {
3054 var c = selected_node.children;
3055 var children = [];
3056 for (var i = 0; i < c.length; i++) {
3057 if (c[i].direction === d) {
3058 children.push(i);
3059 }
3060 }
3061 node = c[children[Math.floor((children.length - 1) / 2)]];
3062 }
3063 else if (selected_node.direction === d) {
3064 var children = selected_node.children;
3065 var childrencount = children.length;
3066 if (childrencount > 0) {
3067 node = children[Math.floor((childrencount - 1) / 2)];
3068 }
3069 } else {
3070 node = selected_node.parent;
3071 }
3072 if (!!node) {
3073 _jm.select_node(node);
3074 }
3075 evt.stopPropagation();
3076 evt.preventDefault();
3077 }
3078 },
3079 };
3080
3081
3082 // plugin
3083 jm.plugin = function (name, init) {
3084 this.name = name;
3085 this.init = init;
3086 };
3087
3088 jm.plugins = [];
3089
3090 jm.register_plugin = function (plugin) {
3091 if (plugin instanceof jm.plugin) {
3092 jm.plugins.push(plugin);
3093 }
3094 };
3095
3096 jm.init_plugins = function (sender) {
3097 $w.setTimeout(function () {
3098 jm._init_plugins(sender);
3099 }, 0);
3100 };
3101
3102 jm._init_plugins = function (sender) {
3103 var l = jm.plugins.length;
3104 var fn_init = null;
3105 for (var i = 0; i < l; i++) {
3106 fn_init = jm.plugins[i].init;
3107 if (typeof fn_init === 'function') {
3108 fn_init(sender);
3109 }
3110 }
3111 };
3112
3113 jm.show = function (options, mind) {
3114 logger.warn('`jsMind.show(options, mind)` is deprecated, please use `jm = new jsMind(options); jm.show(mind);` instead')
3115 var _jm = new jm(options);
3116 _jm.show(mind);
3117 return _jm;
3118 };
3119
3120 // export jsmind
3121 if (typeof module !== 'undefined' && typeof exports === 'object') {
3122 module.exports = jm;
3123 } else if (typeof define === 'function' && (define.amd || define.cmd)) {
3124 define(function () { return jm; });
3125 } else {
3126 $w[__name__] = jm;
3127 }
3128})(typeof window !== 'undefined' ? window : global);