root 3 jaren geleden
bovenliggende
commit
d1cf1e1bda

+ 1 - 1
css/Desktop/Desktop.css

@@ -486,7 +486,7 @@ body div ::-webkit-scrollbar-resizer:vertical {
 .U_MD_D_BZMO {
     margin: -1px 0 0 40px;
     cursor: pointer;
-    width: 43px;
+    width: 63px;
     height: 83px;
     float: left;
 }

+ 1 - 1
js/Desktop/DeskTop.js

@@ -461,7 +461,7 @@ U.MD.D.I.openApplication = function (str, obj, info) {
         case "mind":
             _formdiv = new U.UF.UI.form(
                 "思维导图",
-                $$("iframe", { "style": { "cssText": "border:0;width:100%;height:100%" }, "src": "https://pbl.cocorobo.cn" }), {
+                $$("iframe", { "style": { "cssText": "border:0;width:100%;height:100%" }, "src": "/jsmind/example/demo.html" }), {
                 "id": "mind",
                 "style": { "width": "90%", "height": "90%", "overflow": 'hidden' },
                 "onresize": function () { }

+ 24 - 0
jsmind/LICENSE

@@ -0,0 +1,24 @@
+Copyright (c) 2014-2021, ZHANG ZHIGANG <hizzgdev@163.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+版权所有 (c) 2014-2021, 张志刚 <hizzgdev@163.com>
+保留一切权利。
+
+在满足下列条件的前提下,授予使用者使用及再发布本软件的源代码或二进制形式的权利,无论是否修改皆然:
+
+* 对于本软件源代码的再发布,必须保留上述的版权声明、此三条许可条件,以及下述的免责声明。
+* 对于本软件二进制形式的再发布,必须在随同提供的文档和/或其它媒介中,包含上述的版权声明、此三条许可条件,以及下述的免责声明。
+* 未获得事前书面许可,不得使用版权所有人的名称和贡献者的名字,来为本软件的衍生物做任何支持、认可或推广、促销的行为。
+
+此软件由版权所有者及贡献者以现状("as is")方式提供,本软件不负任何明示或暗示的担保责任,包括但不限于就适销性以及特定目的的适用性的暗示性担保。任何因使用本软件造成的,直接、间接、连带、特别、惩戒或任何结果的损害(包括但不限于替代商品及服务的采购,无法使用,数据丢失,盈利损失或业务中断等),无论任何条件、无论任何原因或任何责任推断、无论是否属于合同范畴、无论是否为严格赔偿责任或民事侵权行为(包括过失或其他原因)而起,即使在使用前已获告知可能会造成此类损害的情形下,版权所有者及贡献者均不负任何责任。
+

+ 61 - 0
jsmind/README.md

@@ -0,0 +1,61 @@
+jsMind
+======
+
+jsMind 是一个显示/编辑思维导图的纯 javascript 类库,其基于 html5 的 canvas 进行设计。jsMind 以 BSD 协议开源,在此基础上你可以在你的项目上任意使用。你可以在此浏览[适用于 jsMind 的 BSD 许可协议(中英文版本)][3]。
+
+jsMind is a pure javascript library for mindmap, it base on html5 canvas. jsMind was released under BSD license, you can embed it in any project, if only you observe the license. You can read [the BSD license agreement for jsMind in English and Chinese version][3] here.
+
+**jsmind 现已发布到 npm https://www.npmjs.com/package/jsmind**
+
+Links:
+
+* App : <http://jsmind.sinaapp.com>
+* Home : <http://hizzgdev.github.io/jsmind/developer.html>
+* Demo :
+  * <http://hizzgdev.github.io/jsmind/example/1_basic.html>
+  * <http://hizzgdev.github.io/jsmind/example/2_features.html>
+* Documents :
+  * [简体中文][1]
+  * [English(draft)][2]
+* Wiki :
+  * [邮件列表 Mailing List](../../wiki/MailingList)
+  * [热点问题 Hot Topics](../../wiki/HotTopics)
+* Donate :
+  * [资助本项目的开发][4]
+
+Get Started:
+
+```html
+<html>
+    <head>
+        <link type="text/css" rel="stylesheet" href="style/jsmind.css" />
+        <script type="text/javascript" src="js/jsmind.js"></script>
+        <!--
+            enable drag-and-drop feature
+            <script type="text/javascript" src="js/jsmind.draggable.js"></script>
+        -->
+    </head>
+    <body>
+        <div id="jsmind_container"></div>
+
+        <script type="text/javascript">
+            var mind = {
+                // 3 data formats were supported ...
+                // see Documents for more information
+            };
+            var options = {
+                container:'jsmind_container',
+                theme:'orange',
+                editable:true
+            };
+            var jm = new jsMind(options);
+            jm.show(mind);
+        </script>
+    </body>
+</html>
+```
+
+[1]:docs/zh/index.md
+[2]:docs/en/index.md
+[3]:LICENSE
+[4]:http://hizzgdev.github.io/jsmind/donate.html

+ 185 - 0
jsmind/docs/en/1.usage.md

@@ -0,0 +1,185 @@
+[Contents](index.md)
+
+1. **Usage**
+2. [Options](2.options.md)
+3. [Operation](3.operation.md)
+4. [Contribution](4.contribution.md)
+
+1.1. Basic Framework
+===
+
+At first, 2 files (jsmind.css and jsmind.js) are required.
+
+```html
+<link type="text/css" rel="stylesheet" href="style/jsmind.css" />
+<script type="text/javascript" src="js/jsmind.js"></script>
+```
+
+add script jsmind.draggable.js for enabling drag-and-drop feature.
+
+```html
+<script type="text/javascript" src="js/jsmind.draggable.js"></script>
+```
+
+The second, a div element should be in your HTML as container
+
+```html
+<div id="jsmind_container"></div>
+```
+
+The last, show an empty mindmap:
+
+```javascript
+<script type="text/javascript">
+    var options = {                     // for more detail at next chapter
+        container:'jsmind_container',   // [required] id of container
+        editable:true,                  // [required] whether allow edit or not
+        theme:'orange'                  // [required] theme
+    };
+    var jm = new jsMind(options);
+    jm.show();
+</script>
+```
+
+Or, show a mindmap with some topics:
+
+```javascript
+<script type="text/javascript">
+    var mind = { /* jsMind data, for more detail at next section */ };
+    var options = {
+        container:'jsmind_container',
+        editable:true,
+        theme:'orange'
+    };
+    var jm = new jsMind(options);
+    // show it
+    jm.show(mind);
+</script>
+```
+
+1.2. Data Format
+===
+
+Three formats are supported by jsMind: `node-tree format`,`node-array format`,`freemind format`. jsMind can load either format below, also can export data for any format.
+
+These are simple demo for the 3 data format:
+
+A. node-tree format
+
+```javascript
+var mind = {
+    "meta":{
+        "name":"jsMind remote",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    "format":"node_tree",
+    "data":{"id":"root","topic":"jsMind","children":[
+        {"id":"easy","topic":"Easy","direction":"left","children":[
+            {"id":"easy1","topic":"Easy to show"},
+            {"id":"easy2","topic":"Easy to edit"},
+            {"id":"easy3","topic":"Easy to store"},
+            {"id":"easy4","topic":"Easy to embed"}
+        ]},
+        {"id":"open","topic":"Open Source","direction":"right","children":[
+            {"id":"open1","topic":"on GitHub"},
+            {"id":"open2","topic":"BSD License"}
+        ]},
+        {"id":"powerful","topic":"Powerful","direction":"right","children":[
+            {"id":"powerful1","topic":"Base on Javascript"},
+            {"id":"powerful2","topic":"Base on HTML5"},
+            {"id":"powerful3","topic":"Depends on you"}
+        ]},
+        {"id":"other","topic":"test node","direction":"left","children":[
+            {"id":"other1","topic":"I'm from local variable"},
+            {"id":"other2","topic":"I can do everything"}
+        ]}
+    ]}
+};
+```
+
+B. node-array format
+
+```javascript
+var mind = {
+    "meta":{
+        "name":"example",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    "format":"node_array",
+    "data":[
+        {"id":"root", "isroot":true, "topic":"jsMind"},
+
+        {"id":"easy", "parentid":"root", "topic":"Easy", "direction":"left"},
+        {"id":"easy1", "parentid":"easy", "topic":"Easy to show"},
+        {"id":"easy2", "parentid":"easy", "topic":"Easy to edit"},
+        {"id":"easy3", "parentid":"easy", "topic":"Easy to store"},
+        {"id":"easy4", "parentid":"easy", "topic":"Easy to embed"},
+
+        {"id":"open", "parentid":"root", "topic":"Open Source", "direction":"right"},
+        {"id":"open1", "parentid":"open", "topic":"on GitHub"},
+        {"id":"open2", "parentid":"open", "topic":"BSD License"},
+
+        {"id":"powerful", "parentid":"root", "topic":"Powerful", "direction":"right"},
+        {"id":"powerful1", "parentid":"powerful", "topic":"Base on Javascript"},
+        {"id":"powerful2", "parentid":"powerful", "topic":"Base on HTML5"},
+        {"id":"powerful3", "parentid":"powerful", "topic":"Depends on you"},
+    ]
+};
+```
+
+C. freemind format
+
+```javascript
+var mind = {
+    "meta":{
+        "name":"example",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    "format":"freemind",
+    "data":"<map version=\"1.0.1\"> <node ID=\"root\" TEXT=\"jsMind\" > <node ID=\"easy\" POSITION=\"left\" TEXT=\"Easy\" > <node ID=\"easy1\" TEXT=\"Easy to show\" /> <node ID=\"easy2\" TEXT=\"Easy to edit\" /> <node ID=\"easy3\" TEXT=\"Easy to store\" /> <node ID=\"easy4\" TEXT=\"Easy to embed\" /> </node> <node ID=\"open\" POSITION=\"right\" TEXT=\"Open Source\" > <node ID=\"open1\" TEXT=\"on GitHub\" /> <node ID=\"open2\" TEXT=\"BSD License\" /> </node> <node ID=\"powerful\" POSITION=\"right\" TEXT=\"Powerful\" > <node ID=\"powerful1\" TEXT=\"Base on Javascript\" /> <node ID=\"powerful2\" TEXT=\"Base on HTML5\" /> <node ID=\"powerful3\" TEXT=\"Depends on you\" /> </node> <node ID=\"other\" POSITION=\"left\" TEXT=\"test node\" > <node ID=\"other1\" TEXT=\"I'm from local variable\" /> <node ID=\"other2\" TEXT=\"I can do everything\" /> </node> </node> </map>"
+};
+```
+
+1.3. Themes
+===
+
+15 themes were supported in jsmind, you can preview those themes by visiting [feature-demo](http://hizzgdev.github.io/jsmind/example/2_features.html).
+
++ primary
++ warning
++ danger
++ success
++ info
++ greensea
++ nephrite
++ belizehole
++ wisteria
++ asphalt
++ orange
++ pumpkin
++ pomegranate
++ clouds
++ asbestos
+
+or, you can add your custom theme in jsmind.css.
+
+```css
+/* greensea theme */
+jmnodes.theme-greensea jmnode{background-color:#1abc9c;color:#fff;}
+jmnodes.theme-greensea jmnode:hover{background-color:#16a085;}
+jmnodes.theme-greensea jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-greensea jmnode.root{}
+jmnodes.theme-greensea jmexpander{}
+jmnodes.theme-greensea jmexpander:hover{}
+```
+
+copyright notice
+===
+
+Reproduction and deduction are prohibited.
+
+The jsMind project is still being updated and the corresponding documentation is updated at the same time as the version is updated. In order to avoid confusion to the user, it is forbidden to reprint this document without written permission and to make changes of any kind to this document.
+

+ 198 - 0
jsmind/docs/en/2.options.md

@@ -0,0 +1,198 @@
+[Contents](index.md)
+
+1. [Usage](1.usage.md)
+2. **Options**
+3. [Operation](3.operation.md)
+4. [Contribution](4.contribution.md)
+
+
+2.1. Introduction
+===
+
+The options object for jsMind was briefly mentioned in the example in the previous chapter:
+
+```javascript
+var options = {
+    container:'jsmind_container', 	// [required] ID of the container
+    editable:true, 					// [Optional] Whether to enable editing
+    theme:'orange' 					// [optional] theme
+};
+var jm = new jsMind(options);
+````
+
+But that's only a small part of it, and the full definition of the options object for jsMind is shown below:
+
+```javascript
+options = {
+   container : '', 			// [required] ID of the container
+   editable : false, 		// Is editing enabled?
+   theme : null, 			// theme
+   mode :'full', 			// display mode
+   support_html : true, 	// Does it support HTML elements in the node?
+   view:{
+       engine: 'canvas', 	// engine for drawing lines between nodes in the mindmap
+       hmargin:100, 		// Minimum horizontal distance of the mindmap from the outer frame of the container
+       vmargin:50, 			// Minimum vertical distance of the mindmap from the outer frame of the container
+       line_width:2, 		// thickness of the mindmap line
+       line_color:'#555' 	// Thought mindmap line color
+   },
+   layout:{
+       hspace:30, 			// horizontal spacing between nodes
+       vspace:20, 			// vertical spacing between nodes
+       pspace:13 			// Horizontal spacing between node and connection line (to place node expander)
+   }, 
+   shortcut:{
+       enable:true, 		// whether to enable shortcut
+       handles:{}, 			// Named shortcut key event processor
+       mapping:{ 			// shortcut key mapping
+           addchild : 45, 	// <Insert>
+           addbrother : 13, // <Enter>
+           editnode : 113, 	// <F2>
+           delnode : 46, 	// <Delete>
+           toggle : 32, 	// <Space>
+           left : 37, 		// <Left>
+           up : 38, 		// <Up>
+           right : 39, 		// <Right>
+           down : 40, 		// <Down>
+       }
+   },
+};
+```
+
+The above options are the default options for jsMind and you can add the appropriate options to override these default options.
+
+Except the container, there are all optional.
+
+These options are described in more detail below.
+
+2.2 Conventional Options
+===
+
+**container** : (string) [required] ID of the container
+
+> This parameter is not default when instantiating a jsMind. jsMind uses this parameter to find a page element and output a mindmap to that element. For easy control of the size and position of the mind map, use [block element][1] as a container for the mind map, such as `<div>`.
+
+> You can give the element retouching, but generally limited to setting its size, position, border, etc.; if you want to change the font, font size, background color, foreground color, etc. of the mindmap, it is recommended to add custom themes by way of processing.
+
+**editable** : (bool) Enable editing or not
+
+> If this parameter is set to true, you can use the API to do the above operations, otherwise the API will not take effect. By default, the value of this parameter is false .
+
+> Note that jsMind only provides an editing interface and a small amount of shortcut key support, not full editing functionality, and this parameter is only used to limit the use of these APIs.
+
+**theme** : (string) subject
+
+> Specify the theme name of jsMind. (look in jsMind.css)
+
+**mode** : (string) display mode
+
+> jsMind now supports two display modes:
+
+> * full - child nodes are dynamically distributed on both sides of the root node [default]
+> * side - child nodes are distributed only to the right of the root node
+
+**support_html** : (bool) Does it support HTML elements in nodes?
+
+> The default value of this parameter is true, meaning that it allows HTML code to be used in the node's header, and you can even insert a table `<table>` in the node's header if you wish. If you want only plain text in the title, set this parameter to false .
+
+> Note that in freemind, the style of the node is controlled using the HTML language, and it is recommended to set this parameter to true if you are using data in freemind format.
+
+2.3 Layout Options
+===
+
+**view.engine** : (string) engine for drawing lines between nodes in a mindmap
+
+> jsMind now supports two line drawing engines:
+
+> * CANVAS - Draw the lines on the canvas [default]
+> * SVG - Using SVG to draw lines, when there are a lot of nodes and a huge area in the mind map, using this mode can bring significant performance improvements
+
+**view.hmargin** : (number) Minimum horizontal distance (in pixels) of the mindmap from the container frame  
+**view.vmargin** : (number) Minimum vertical distance (in pixels) of the mindmap from the outer container frame
+
+> These two parameters determine how close the mindmap can be to the border of the container. These two parameters are similar to the margin(css) property of the object if you consider the thought map itself as an object. For aesthetic purposes, the default setting is 100 pixels in the horizontal direction and 50 pixels in the vertical direction.
+
+**view.line_width** : (number) Thickness of the mindmap line (pixels)  
+**view.line_color** : (string) color of the mindmap line (color representation in HTML)
+
+> These two parameters determine the style of the lines in the mindmap. By default, the lines are 2px in thickness and dark gray in color (#555).
+
+**layout.hspace** : horizontal distance (pixels) between (number) nodes  
+**layout.vspace** : vertical spacing (pixels) between (number) nodes
+
+> These two parameters are equivalent to the margin(css) property of the node object, which defaults to 30 pixels in the horizontal direction and 20 pixels in the vertical direction.
+
+**layout.pspace** : (number) size of node expander (pixels)
+
+> If a node (other than the root node) has a subordinate node, the outside of this node shows the controller for the shrinking/expanding subordinate node, which is used to set the size (width and height) of this controller, the default value is 13 pixels.
+
+2.4. Shortcuts
+===
+
+**shortcut.enable** : (bool) Whether to enable shortcut keys
+
+> This parameter is used to control whether you can use keyboard shortcuts to edit (or otherwise manipulate) a mindmap in the jsMind interface, the default value is true, i.e. shortcuts are enabled.
+
+**shortcut.handles** : (object{string : function}) Named shortcut event handler
+
+> jsMind provides some common event handler for manipulating mindmaps (see next section), and this parameter provides the ability to define additional event handler.
+> This parameter is a collection of string->function(jsmind,event), string specifies the name of the event handler, and function is the logic to be executed by the event handler, in the next section of shortcut.mapping configuration, the name of the processor will be bound to the specific keys for the purpose of shortcut operation. For example, the following code defines a processor.
+
+```javascript
+handles : {
+    'dosomething' : function(jm,e){
+        // do something...
+    },
+    'dosomeotherthing' : function(jm,e){
+        // do some other things
+    }
+    ...
+}
+```
+
+**shortcut.mapping** : (object{string : number}) Shortcut mapping configuration
+
+> This parameter is used to configure the correspondence between a specific key and the event handler, this code shows the correspondence by default, e.g. the code for the [Insert] key is 45 and can be used to add a child node, while 112 represents the [F1] key for dosomething.
+
+```javascript
+mapping:{ 				// handle mapping.
+   addchild : 45, 		// <Insert>
+   addbrother : 13, 	// <Enter>
+   editnode : 113, 		// <F2>
+   delnode : 46, 		// <Delete>
+   toggle : 32, 		// <Space>
+   left : 37, 			// <Left>
+   up : 38, 			// <Up>
+   right : 39, 			// <Right>
+   down : 40, 			// <Down>
+
+   // Examples
+   dosomething: 112, 	// <F1>
+}
+```
+
+> In addition to the single-key scenario described above, jsMind has added support for combination keys, where the code of the combination shortcut is the code of the regular key plus the identification code of the function key.
+> Four function keys are currently supported, and the corresponding identification codes are.
+
+> * Meta : 8192 (jsMind.key.meta)
+> * Ctrl : 4096 (jsMind.key.ctrl)
+> * ALT : 2048 (jsMind.key.alt)
+> * SHIFT : 1024 (jsMind.key.shift)
+
+> The following are some examples.
+
+```javascript
+mapping:{
+   addchild : jsMind.key.ctrl + 73, 					// <Ctrl> + <I>
+   delnode : jsMind.key.ctrl + jsMind.key.alt + 68, 	// <Ctrl> + <ALT> + <D>
+}
+```
+
+copyright notice
+===
+
+Reproduction and deduction are prohibited.
+
+The jsMind project is still being updated and the corresponding documentation is updated at the same time as the version is updated. In order to avoid confusion to the user, it is forbidden to reprint this document without written permission and to make changes of any kind to this document.
+
+[1]:http://www.nowamagic.net/librarys/veda/detail/1190 "CSS Block Level Element, Inline Element Concept"

+ 138 - 0
jsmind/docs/en/3.operation.md

@@ -0,0 +1,138 @@
+[Contents](index.md)
+
+1. [Usage](1.usage.md)
+2. [Options](2.options.md)
+3. **Operation**
+4. [Contribution](4.contribution.md)
+
+
+jsMind object
+===
+
+jsMind provides a set of APIs for manipulating the mindmap, all of which are based on the `jsMind` object processing, which can be obtained using the following code.
+
+```javascript
+/*
+Method 1.
+    jsMind objects are available when you create a mindmap
+*/
+var jm = new jsMind(options);
+
+/*
+Method 2.
+    This jsMind object can be obtained directly if a mindmap already exists on the current page
+    When multiple jsMind are created on a page, this method gets the last object created
+*/
+var jm = jsMind.current;
+```
+
+3.1 Displaying a mindmap
+===
+
+Use the `jm.show(mind)` method to display a mindmap, see [1.1. Basic framework](1.usage.md) for specific usage.
+
+3.2. Finding Nodes
+===
+
+**Get root** : Use `jm.get_root()` to get the root of the current mindmap.
+
+**Find node by id** : Use the `jm.get_node(nodeid)` method to find the node specified in the current mind map by id, and return `null` if it is not found.
+
+**Get selected node** : Use `jm.get_selected_node()` to get the currently selected node and return `null` if there is no selected node.
+
+**Find adjacent nodes** : Use `jm.find_node_before(node|nodeid)` and `find_node_after(node|nodeid)` to get the previous or next node of the specified node, and return `null` if there is no previous or next one.
+
+**Fetch parent** : Use `node.parent` to get the parent node, the parent of the root node is `null`.
+
+**Fetching a collection of child nodes** : A collection of child nodes can be obtained using `node.children`.
+
+Tips
+---
+
+A mindmap diagram is composed of multiple nodes and connections between nodes, a mindmap diagram has a root node, the root node can have multiple child nodes on the periphery, and the child node can have multiple child nodes. Each node contains more than one of the following attributes.
+
+```javascript
+node {
+    id, 		// : string node id
+    index, 		// : integer node number
+    topic, 		// : string node topic
+    isroot, 	// : boolean indicates whether this node is root or not
+    parent, 	// : node The parent of this node, the parent program of the root node is null, but please do not judge whether this node is the root node based on this property.
+    direction, 	// : enum(left,center,right) The distribution position of the node
+    children, 	// : array of node The combination of children of the node
+    expanded, 	// : boolean Is the next level of the node expanded or not
+    data, 		// : object{string,object} Other additional data for this node
+}
+```
+
+3.3 Operation on Nodes
+===
+
+**Select node** : Use the `jm.select_node(node) method to select the specified node.
+
+**Collapse child nodes** : Use the `jm.collapse_node(node|nodeid)` method to collapse the child nodes of the node.
+
+**Expand Child Node** : Use the `jm.expand_node(node|nodeid)` method to expand the child node of this node.
+
+**Collapse or expand child nodes** : Use the `jm.toggle_node(node|nodeid)` method to automatically expand or collapse child nodes.
+
+**Expand all child nodes** : All child nodes can be expanded using the `jm.expand_all()` method.
+
+**Expand to level** : Use the `jm.expand_to_depth(depth)` method to expand to a specified level.
+
+**Move Node** : Use the `jm.move_node(node|nodeid,beforeid)` method to move the node to beforeid node, and set beforeid to `_first_` or `_last` to move the node to first or last of the adjacent node. 
+
+**Enable editing** : Use the `jm.enable_edit()` method to enable editing of the current mindmap.
+
+**Disable_edit** : Use the `jm.disable_edit()` method to disable editing of the current mindmap.
+
+**Edit Node** : The node can be adjusted to edit using the `jm.begin_edit(node|nodeid)` method.
+
+**Stop editing** : The node can be adjusted to read-only using the `jm.end_edit()` method.
+
+
+3.4. Editing Nodes
+===
+
+**Add Node** : A node can be added using the `jm.add_node(parent_node, nodeid, topic, data)` method.
+
+**Insert node before specified location** : The `jm.insert_node_before(node_before, nodeid, topic, data)` method can be used to insert a node before a node_before node.
+
+**Insert node after specified location** : Node can be inserted after node_after using `jm.insert_node_after(node_after, nodeid, topic, data)` method.
+
+**Delete Node** : Use the `jm.remove_node(node|nodeid)` method to delete a specified node and all its children.
+
+**Update node** : Use the `jm.update_node(nodeid, topic)` method to update the topic of the specified node.
+
+
+3.5 Setting Style
+===
+
+**Set the theme** : Use the `jm.set_theme(theme)` method to set the theme of the mind map.
+
+**Set background / foreground color** : Use the `jm.set_node_color(nodeid, bgcolor, fgcolor)` method to set the background and foreground color of the specified node.
+
+**Set Font** : Use the `jm.set_node_font_style(nodeid, size, weight, style)` method to set the font of the specified node.
+
+**Set background image** : Use the `jm.set_node_background_image(nodeid, image, width, height)` method to set the background image of the specified node.
+
+3.6 Access to Data
+===
+
+**Get metadata** : Use the `jm.get_meta()` method to get metadata for the current mindmap.
+
+**Get Data** : The `jm.get_data(data_format)` method is used to get the data text in the specified format of the current mindmap.
+
+3.7 Other Operations
+===
+
+**Clear selection of nodes** : Use the `jm.select_clear()` method to clear the current selected state.
+
+**Determine if the node is visible** : Use the `jm.is_node_visible(node)` method to determine if this node is visible.
+
+copyright notice
+===
+
+Reproduction and deduction are prohibited.
+
+The jsMind project is still being updated and the corresponding documentation is updated at the same time as the version is updated. In order to avoid confusion to the user, it is forbidden to reprint this document without written permission and to make changes of any kind to this document.

+ 34 - 0
jsmind/docs/en/4.contribution.md

@@ -0,0 +1,34 @@
+[Contents](index.md)
+
+1. [Usage](1.usage.md)
+2. [Options](2.options.md)
+3. [Operation](3.operation.md)
+4. **Contribution**
+
+4.1. contribution code
+===
+
+jsMind may not meet the needs of your project in some ways, and you are very welcome to extend jsMind and give feedback by contributing code.
+
+The most convenient way to contribute code is to submit a pull-request to the jsMind project, which can be done in the github help documentation.
+
+* :: [Creating a pull request from a fork](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork)
+* :: [Working with forks] (https://help.github.com/en/articles/working-with-forks)
+
+Before submitting a pull-request, be sure to test it well and make a detailed note when submitting the pull-request, including feature descriptions, code change notes.
+
+4.2 Contribute ideas or needs
+===
+
+Or you can bring up a problem or need you've encountered via an issue for discussion, and hopefully a friend who has the ability and energy to join in and solve the problem together.
+
+For common requirements, it is recommended to implement post-implementation feedback to the open source project, for non-common requirements, you can fork out a separate warehouse for custom development.
+
+It is important to note that open source does not mean free. jsMind itself doesn't restrict commercial use, but it's only right to be paid for custom development.
+
+copyright notice
+===
+
+Reproduction and deduction are prohibited.
+
+The jsMind project is still being updated and the corresponding documentation is updated at the same time as the version is updated. In order to avoid confusion to the user, it is forbidden to reprint this document without written permission and to make changes of any kind to this document.

+ 28 - 0
jsmind/docs/en/index.md

@@ -0,0 +1,28 @@
+Contents.
+======
+
+* [1. Usage](1.usage.md)
+  * 1.1 Basic Framework
+  * 1.2 Data Format
+  * 1.3 Themes
+* [2. Options](2.options.md)
+  * 2.1 Introduction
+  * 2.2 Conventional Options
+  * 2.3 Layout Options
+  * 2.4 Shortcuts
+* [3. API / Operation](3.operation.md)
+  * 3.1 Displaying a Mindmap
+  * 3.2 Finding Nodes
+  * 3.3 Operation on Nodes
+  * 3.4 Editing Nodes
+  * 3.5 Setting Style
+  * 3.6 Access to Data
+  * 3.7 Other Operations
+* [4. Contribution](4.contribution.md)
+
+copyright notice
+======
+
+Reproduction and deduction are prohibited.
+
+The jsMind project is still being updated and the corresponding documentation is updated at the same time as the version is updated. In order to avoid confusion to the user, it is forbidden to reprint this document without written permission and to make changes of any kind to this document.

+ 217 - 0
jsmind/docs/zh/1.usage.md

@@ -0,0 +1,217 @@
+[目录](index.md)
+
+1. **基本用法**
+2. [选项](2.options.md)
+3. [界面操控](3.operation.md)
+4. [参与贡献](4.contribution.md)
+
+1.1. 基本框架
+===
+
+首先,需要在页面上引用 jsmind.js 和 jsmind.css 两个文件。
+
+```html
+<link type="text/css" rel="stylesheet" href="style/jsmind.css" />
+<script type="text/javascript" src="js/jsmind.js"></script>
+```
+
+如果希望能够通过鼠标拖拽的方式移动节点,需要额外引用 jsmind.draggable.js 文件
+
+```html
+<script type="text/javascript" src="js/jsmind.draggable.js"></script>
+```
+
+其次,要为 jsMind 准备一个容器,jsMind 将在这个容器里显示思维导图。可自行定义容器的id、大小及样式。
+
+```html
+<div id="jsmind_container"></div>
+```
+
+最后,添加下面一段代码即可显示一个空白的思维导图:
+
+```javascript
+<script type="text/javascript">
+    var options = {                   // options 将在下一章中详细介绍
+        container:'jsmind_container', // [必选] 容器的ID,或者为容器的对象
+        editable:true,                // [可选] 是否启用编辑
+        theme:'orange'                // [可选] 主题
+    };
+    var jm = new jsMind(options);
+    jm.show();
+</script>
+```
+
+或者,使用下面的代码显示一个包含既定内容的思维导图:
+
+```javascript
+<script type="text/javascript">
+    var mind = { /* jsMind 数据,详见下一节的说明 */ };
+    var options = {
+        container:'jsmind_container',
+        editable:true,
+        theme:'orange'
+    };
+
+    var jm = new jsMind(options);
+    // 让 jm 显示这个 mind 即可
+    jm.show(mind); 
+</script>
+```
+
+1.2. 数据格式
+===
+
+jsMind 支持三种数据格式,分别是树对象格式、表对象格式、freemind格式。jsMind 可以加载其中任一种格式,也能将数据导出为任一种格式。
+
+* **树对象格式** 默认格式,节点之间是包含关系,便于程序进行处理,适合与 MongoDB 及其它文档型数据库进行数据交互;
+* **表对象格式** 节点之间是并列关系,使用 parentid 标识上下级关系,适合与关系型数据库进行数据交互;
+* **freemind格式** 使用 freemind 的 xml 格式,适合与 freemind 进行数据交互。
+
+下面是三种数据格式的简单示例:
+
+A. 树对象格式示例
+---
+
+```javascript
+var mind = {
+    /* 元数据,定义思维导图的名称、作者、版本等信息 */
+    "meta":{
+        "name":"jsMind-demo-tree",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    /* 数据格式声明 */
+    "format":"node_tree",
+    /* 数据内容 */
+    "data":{"id":"root","topic":"jsMind","children":[
+        {"id":"easy","topic":"Easy","direction":"left","expanded":false,"children":[
+            {"id":"easy1","topic":"Easy to show"},
+            {"id":"easy2","topic":"Easy to edit"},
+            {"id":"easy3","topic":"Easy to store"},
+            {"id":"easy4","topic":"Easy to embed"}
+        ]},
+        {"id":"open","topic":"Open Source","direction":"right","expanded":true,"children":[
+            {"id":"open1","topic":"on GitHub"},
+            {"id":"open2","topic":"BSD License"}
+        ]},
+        {"id":"powerful","topic":"Powerful","direction":"right","children":[
+            {"id":"powerful1","topic":"Base on Javascript"},
+            {"id":"powerful2","topic":"Base on HTML5"},
+            {"id":"powerful3","topic":"Depends on you"}
+        ]},
+        {"id":"other","topic":"test node","direction":"left","children":[
+            {"id":"other1","topic":"I'm from local variable"},
+            {"id":"other2","topic":"I can do everything"}
+        ]}
+    ]}
+};
+```
+
+B. 表对象格式示例
+---
+
+```javascript
+var mind = {
+    /* 元数据,定义思维导图的名称、作者、版本等信息 */
+    "meta":{
+        "name":"example",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    /* 数据格式声明 */
+    "format":"node_array",
+    /* 数据内容 */
+    "data":[
+        {"id":"root", "isroot":true, "topic":"jsMind"},
+
+        {"id":"easy", "parentid":"root", "topic":"Easy", "direction":"left"},
+        {"id":"easy1", "parentid":"easy", "topic":"Easy to show"},
+        {"id":"easy2", "parentid":"easy", "topic":"Easy to edit"},
+        {"id":"easy3", "parentid":"easy", "topic":"Easy to store"},
+        {"id":"easy4", "parentid":"easy", "topic":"Easy to embed"},
+
+        {"id":"open", "parentid":"root", "topic":"Open Source", "expanded":false, "direction":"right"},
+        {"id":"open1", "parentid":"open", "topic":"on GitHub"},
+        {"id":"open2", "parentid":"open", "topic":"BSD License"},
+
+        {"id":"powerful", "parentid":"root", "topic":"Powerful", "direction":"right"},
+        {"id":"powerful1", "parentid":"powerful", "topic":"Base on Javascript"},
+        {"id":"powerful2", "parentid":"powerful", "topic":"Base on HTML5"},
+        {"id":"powerful3", "parentid":"powerful", "topic":"Depends on you"},
+    ]
+};
+```
+
+C. freemind格式示例
+---
+
+```javascript
+var mind = {
+    /* 元数据,定义思维导图的名称、作者、版本等信息 */
+    "meta":{
+        "name":"example",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    /* 数据格式声明 */
+    "format":"freemind",
+    /* 数据内容 */
+    "data":"<map version=\"1.0.1\"> <node ID=\"root\" TEXT=\"jsMind\" > <node ID=\"easy\" POSITION=\"left\" TEXT=\"Easy\" > <node ID=\"easy1\" TEXT=\"Easy to show\" /> <node ID=\"easy2\" TEXT=\"Easy to edit\" /> <node ID=\"easy3\" TEXT=\"Easy to store\" /> <node ID=\"easy4\" TEXT=\"Easy to embed\" /> </node> <node ID=\"open\" POSITION=\"right\" TEXT=\"Open Source\" > <node ID=\"open1\" TEXT=\"on GitHub\" /> <node ID=\"open2\" TEXT=\"BSD License\" /> </node> <node ID=\"powerful\" POSITION=\"right\" TEXT=\"Powerful\" > <node ID=\"powerful1\" TEXT=\"Base on Javascript\" /> <node ID=\"powerful2\" TEXT=\"Base on HTML5\" /> <node ID=\"powerful3\" TEXT=\"Depends on you\" /> </node> <node ID=\"other\" POSITION=\"left\" TEXT=\"test node\" > <node ID=\"other1\" TEXT=\"I'm from local variable\" /> <node ID=\"other2\" TEXT=\"I can do everything\" /> </node> </node> </map>"
+};
+```
+
+注
+---
+
+除 freemind 格式外,其余两种格式的基本数据结构如下:
+
+```javascript
+
+    {
+        "id":"open",           // [必选] ID, 所有节点的ID不应有重复,否则ID重复的结节将被忽略
+        "topic":"Open Source", // [必选] 节点上显示的内容
+        "direction":"right",   // [可选] 节点的方向,此数据仅在第一层节点上有效,目前仅支持 left 和 right 两种,默认为 right
+        "expanded":true,       // [可选] 该节点是否是展开状态,默认为 true
+    }
+
+```
+
+1.3. 主题
+===
+
+jsMind 默认提供了 15 种主题,你可以访问[功能示例](http://hizzgdev.github.io/jsmind/example/2_features.html)页面浏览这些主题。
+
+* primary
+* warning
+* danger
+* success
+* info
+* greensea
+* nephrite
+* belizehole
+* wisteria
+* asphalt
+* orange
+* pumpkin
+* pomegranate
+* clouds
+* asbestos
+
+当然,你也可以添加自己的主题。只需在 jsmind.css 中按照以下格式添加样式定义即可:
+
+```css
+/* greensea theme */                                                      /* greensea 即是主题名 */
+jmnodes.theme-greensea jmnode{background-color:#1abc9c;color:#fff;}       /* 节点样式 */
+jmnodes.theme-greensea jmnode:hover{background-color:#16a085;}            /* 鼠标悬停的节点样式 */
+jmnodes.theme-greensea jmnode.selected{background-color:#11f;color:#fff;} /* 选中的节点样式 */
+jmnodes.theme-greensea jmnode.root{}                                      /* 根节点样式 */
+jmnodes.theme-greensea jmexpander{}                                       /* 展开/关闭节点的控制点样式 */
+jmnodes.theme-greensea jmexpander:hover{}                                 /* 鼠标悬停展开/关闭节点的控制点样式 */
+```
+
+版权声明
+===
+
+禁止转载、禁止演绎。
+
+jsMind 项目仍在不断升级变化,版本更新时会同时更新对应的文档。为避免给使用者带来困惑,在没有得到书面许可前,禁止转载本文档,同时禁止对本文档进行任何形式的更改。

+ 195 - 0
jsmind/docs/zh/2.options.md

@@ -0,0 +1,195 @@
+[目录](index.md)
+
+1. [基本用法](1.usage.md)
+2. **选项**
+3. [界面操控](3.operation.md)
+4. [参与贡献](4.contribution.md)
+
+2.1. 综述
+===
+
+上一章的示例中简单提到了 jsMind 的 options 对象:
+
+```javascript
+var options = {
+    container:'jsmind_container', // [必选] 容器的ID
+    editable:true,                // [可选] 是否启用编辑
+    theme:'orange'                // [可选] 主题
+};
+var jm = new jsMind(options);
+```
+
+不过这只是很少的一部分,jsMind 的 options 对象的完整定义如下所示:
+
+```javascript
+options = {
+   container : '',         // [必选] 容器的ID
+   editable : false,       // 是否启用编辑
+   theme : null,           // 主题
+   mode :'full',           // 显示模式
+   support_html : true,    // 是否支持节点里的HTML元素
+   view:{
+       engine: 'canvas',   // 思维导图各节点之间线条的绘制引擎
+       hmargin:100,        // 思维导图距容器外框的最小水平距离
+       vmargin:50,         // 思维导图距容器外框的最小垂直距离
+       line_width:2,       // 思维导图线条的粗细
+       line_color:'#555'   // 思维导图线条的颜色
+   },
+   layout:{
+       hspace:30,          // 节点之间的水平间距
+       vspace:20,          // 节点之间的垂直间距
+       pspace:13           // 节点与连接线之间的水平间距(用于容纳节点收缩/展开控制器)
+   },
+   shortcut:{
+       enable:true,        // 是否启用快捷键
+       handles:{},         // 命名的快捷键事件处理器
+       mapping:{           // 快捷键映射
+           addchild   : 45,    // <Insert>
+           addbrother : 13,    // <Enter>
+           editnode   : 113,   // <F2>
+           delnode    : 46,    // <Delete>
+           toggle     : 32,    // <Space>
+           left       : 37,    // <Left>
+           up         : 38,    // <Up>
+           right      : 39,    // <Right>
+           down       : 40,    // <Down>
+       }
+   },
+};
+```
+
+以上选项是 jsMind 的默认选项,除 container 之外,其它选项都是可选的,你可以添加相应的选项以覆盖这些默认选项。
+
+下面将对这些选项进行详细介绍。
+
+2.2. 常规选项
+===
+
+**container** : (string) [必选] 容器的ID
+
+> 实例化一个 jsMind 时,此参数不可缺省。jsMind 通过此参数查找页面元素,并将思维导图输出到该元素中。为了便于控制思维导图的大小和位置,请使用[块元素][1]作为思维导图的容器,如`<div>`。
+
+> 你可以给该元素进行修饰,但是一般仅限于设置其大小、位置、边框等;如果想改变思维导图的字体、字号、背景颜色、前景颜色等,建议通过添加自定义主题的方式进行处理。
+
+**editable** : (bool) 是否启用编辑
+
+> 该参数控制是否支持在思维导图上进行编辑,jsMind 目前提供的编辑行为有添加节点、删除节点、修改节点标题、移动节点位置等,如果该参数设置为 true,将可以使用 API 进行以上这些操作,否则这些 API 将不会生效。默认情况下,该参数的值为 false 。
+
+> 需要注意的是,jsMind 仅提供了编辑接口和少量的快捷键支持,并未提供完整的编辑功能,此参数仅用于限制这些 API 的使用。
+
+**theme** : (string) 主题
+
+> 指定 jsMind 的主题名。
+
+**mode** : (string) 显示模式
+
+> jsMind 现支持两种显示模式:
+
+> * full - 子节点动态分布在根节点两侧 [默认值]
+> * side - 子节点只分布在根节点右侧
+
+**support_html** : (bool) 是否支持节点里的HTML元素
+
+> 该参数的默认值为 true ,含义为允许在节点的标题中使用 HTML 代码,如果你愿意,你甚至可以在节点标题里插入一个表格`<table>`。如果你希望标题中只有纯文本,请将该参数设为 false 。
+
+> 需要注意的是,在 freemind 中,节点的样式是使用 html 语言进行控制的,如果你使用 freemind 格式的数据时,建议将此参数设置为 true。
+
+2.3. 排版选项
+===
+
+**view.engine** : (string) 思维导图各节点之间线条的绘制引擎
+
+> jsMind 现支持两种线条的绘制引擎:
+
+> * canvas - 把线条绘制在 canvas 上 [默认值]
+> * svg - 使用 svg 绘制线条,当思维导图的节点很多,面积巨大的时候,使用该模式能带来显著的性能提升
+
+**view.hmargin** : (number) 思维导图距容器外框的最小水平距离(像素)  
+**view.vmargin** : (number) 思维导图距容器外框的最小垂直距离(像素)
+
+> 这两个参数决定了思维导图与容器的边框能距离多近。把思维导图本身作为一个对象的话,这两个参数就类似该对象的 margin(css) 属性。为了美观起见,水平方向上默认设置为 100 像素,垂直方向上默认设置为 50 像素。
+
+**view.line_width** : (number) 思维导图线条的粗细(像素)  
+**view.line_color** : (string) 思维导图线条的颜色(html的颜色表示方法)
+
+> 这两个参数决定了思维导图中的线条的样式。默认情况下,线条的粗细是2px,颜色是深灰色(#555)。
+
+**layout.hspace** : (number) 节点之间的水平间距(像素)  
+**layout.vspace** : (number) 节点之间的垂直间距(像素)
+
+> 这两个参数相当于节点对象的 margin(css)属性,水平方向上默认值为 30 像素,垂直方向上默认值为 20 像素。
+
+**layout.pspace** : (number) 节点收缩/展开控制器的尺寸(像素)
+
+> 如果某一节点(根节点除外)存在下级节点,则此节点外侧则会显示出收缩/展开下级节点的控制器,此参数用于设置此控制器的大小(宽和高),默认值为 13 像素。
+
+2.4. 快捷键
+===
+
+**shortcut.enable** : (bool) 是否启用快捷键
+
+> 该参数用于控制是否可以在jsMind界面上使用键盘快捷键对思维导图进行编辑(或其它操作),默认值为 true,即启用快捷键。
+
+**shortcut.handles** : (object{string : function}) 命名的快捷键事件处理器
+
+> jsMind提供了一些常用的处理器,用于操作思维导图(参见下一节),该参数提供了定义额外处理器的能力。
+> 该参数是一个 string->function(jsmind,event) 的集合,string 指名了该处理器的名称,function 则是这个处理器具体要执行的逻辑,在下一节的 shortcut.mapping 的配置中,将把处理器的名称绑定到具体的按键上,实现快捷操作的目的。比如以下代码就定义了一个处理器:
+
+```javascript
+handles : {
+    'dosomething' : function(jm,e){
+        // do something...
+    },
+    'dosomeotherthing' : function(jm,e){
+        // do some other things
+    }
+    ...
+}
+```
+
+**shortcut.mapping** : (object{string : number}) 快捷键映射配置
+
+> 该参数用于配置具体的按键与处理器之间的对应关系,此代码显示了默认情况下的对应关系,如 [Insert] 键的代码是 45 ,可用于添加一个子节点,而 112 代表的是 [F1] 键,用于 dosomething。
+
+```javascript
+mapping:{              // handle mapping.
+   addchild   : 45,    // <Insert>
+   addbrother : 13,    // <Enter>
+   editnode   : 113,   // <F2>
+   delnode    : 46,    // <Delete>
+   toggle     : 32,    // <Space>
+   left       : 37,    // <Left>
+   up         : 38,    // <Up>
+   right      : 39,    // <Right>
+   down       : 40,    // <Down>
+
+   // 示例
+   dosomething: 112,   // <F1>
+}
+```
+
+> 除了上述这种单一按键的情况,jsMind新增了对组合按键的支持,组合快捷键的代码为常规按键的代码加上功能键的标识代码。
+> 目前支持四种功能键,对应的标识代码分别为:
+
+> * Meta  : 8192 (jsMind.key.meta)
+> * Ctrl  : 4096 (jsMind.key.ctrl)
+> * ALT   : 2048 (jsMind.key.alt)
+> * SHIFT : 1024 (jsMind.key.shift)
+
+> 以下是一些示例:
+
+```javascript
+mapping:{
+   addchild  : jsMind.key.ctrl + 73,                    // <Ctrl> + <I>
+   delnode   : jsMind.key.ctrl + jsMind.key.alt + 68,   // <Ctrl> + <ALT> + <D>
+}
+```
+
+版权声明
+===
+
+禁止转载、禁止演绎。
+
+jsMind 项目仍在不断升级变化,版本更新时会同时更新对应的文档。为避免给使用者带来困惑,在没有得到书面许可前,禁止转载本文档,同时禁止对本文档进行任何形式的更改。
+
+[1]:http://www.nowamagic.net/librarys/veda/detail/1190 "CSS块级元素、内联元素概念"

+ 135 - 0
jsmind/docs/zh/3.operation.md

@@ -0,0 +1,135 @@
+[目录](index.md)
+
+1. [基本用法](1.usage.md)
+2. [选项](2.options.md)
+3. **界面操控**
+4. [参与贡献](4.contribution.md)
+
+jsMind 对象
+===
+
+jsMind 提供了对思维导图进行操控的一系列 API,这些 API 都是基于 `jsMind` 对象处理的,一般情况下可以使用下面的代码获取 jsMind 对象:
+
+```javascript
+/*
+方法1:
+    创建思维导图时即可获得 jsMind 对象
+*/
+var jm = new jsMind(options);
+
+/*
+方法2:
+    当前页面已存在一个思维导图时可直接获得此 jsMind 对象
+    当在一个页面里创建了多个 jsMind 时,此方法获得的是最后创建的那个对象
+*/
+var jm = jsMind.current;
+```
+
+3.1. 显示思维导图
+===
+
+使用 `jm.show(mind)` 方法即可显示思维导图了,具体的用法请参见 [1.1. 基本框架](1.usage.md)
+
+3.2. 查找节点
+===
+
+**获取根节点** : 使用 `jm.get_root()` 即可获取当前思维导图的根节点。
+
+**根据 id 查找节点** : 使用 `jm.get_node(nodeid)` 方法即可根据 id 查找当前思维导图中指定的节点,如果查找不到则返回 `null`。
+
+**获取选中的节点** : 使用 `jm.get_selected_node()` 方法即可获取当前选中的节点,如果没有选中的节点则返回 `null`。
+
+**查找相邻的节点** : 使用 `jm.find_node_before(node|nodeid)` 和 `find_node_after(node|nodeid)` 即可获取指定的节点的上一个或下一个节点,如果没有上一个或下一个,则返回 `null`。
+
+**获取父节点** : 使用 `node.parent` 即可获取父节点,根节点的父节点为 `null`。
+
+**获取子节点集合** : 使用 `node.children` 即可获取子节点的集合。
+
+Tips
+---
+
+思维导图是由多个节点和节点之间的连线组成的,一个思维导图有一个根节点,根节点外围可以有多个子节点,子节点还可以有多个子节点。每个节点包含以下的多个属性:
+
+```javascript
+node {
+    id,        //  : string                    节点id
+    index,     //  : integer                   节点序号
+    topic,     //  : string                    节点主题
+    isroot,    //  : boolean                   指示该节点是否为根节点
+    parent,    //  : node                      该节点的父节点,根节点的父节目为 null ,但请不要根据此属性判断该节点是否为根节点
+    direction, //  : enum(left,center,right)   该节点的分布位置
+    children,  //  : array of node             该节点的子节点组合
+    expanded,  //  : boolean                   该节点的下级节点是否展开
+    data,      //  : object{string,object}     该节点的其它附加数据
+}
+```
+
+3.3. 操作节点
+===
+
+**选中节点** : 使用 `jm.select_node(node) 方法选中指定的节点。
+
+**收起子节点** : 使用 `jm.collapse_node(node|nodeid)` 方法可收起该节点的子节点。
+
+**展开子节点** : 使用 `jm.expand_node(node|nodeid)` 方法可展开该节点的子节点。
+
+**收起或展开子节点** : 使用 `jm.toggle_node(node|nodeid)` 方法可自动展开或收起子节点。
+
+**展开全部子节点** : 使用 `jm.expand_all()` 方法可展开全部子节点。
+
+**展开至层级** : 使用 `jm.expand_to_depth(depth)` 方法可展开到指定层级。
+
+**移动节点** : 使用 `jm.move_node(node|nodeid,beforeid)` 方法可将该节点移动到 beforeid 节点之前,可将 beforeid 设为 `_first_`或`_last`可将该节点移动到相邻节点的最前或最后。 
+
+**启用编辑** : 使用 `jm.enable_edit()` 方法可启用对当前思维导图的编辑功能。
+
+**禁止编辑** : 使用 `jm.disable_edit()` 方法可禁止对当前思维导图进行编辑。
+
+**编辑节点** : 使用 `jm.begin_edit(node|nodeid)` 方法可以将该节点调整为编辑状态。
+
+**停止编辑** : 使用 `jm.end_edit()` 方法可以将该节点调整为只读状态。
+
+3.4. 编辑节点
+===
+
+**添加节点** : 使用 `jm.add_node(parent_node, nodeid, topic, data)` 方法可添加一个节点。
+
+**在指定位置前插入节点** : 使用 `jm.insert_node_before(node_before, nodeid, topic, data)` 方法可在 node_before 节点前插入节点。
+
+**在指定位置后插入节点** : 使用 `jm.insert_node_after(node_after, nodeid, topic, data)` 方法可在 node_after 节点后插入节点。
+
+**删除节点** : 使用 `jm.remove_node(node|nodeid)` 方法可删除指定的节点及其所有的子节点。
+
+**更新节点** : 使用 `jm.update_node(nodeid, topic)` 方法可更新指定节点的 topic,其它属性由于不在界面上显示,可以直接修改对应 node 的属性。
+
+3.5. 设置样式
+===
+
+**设置主题** : 使用 `jm.set_theme(theme)` 方法可设置当前思维导图的主题。
+
+**设置背景色/前景色** : 使用 `jm.set_node_color(nodeid, bgcolor, fgcolor)` 方法可设置指定节点的背景色与前景色。
+
+**设置字体** : 使用 `jm.set_node_font_style(nodeid, size, weight, style)` 方法可设置指定节点的字体。
+
+**设置背景图片** : 使用 `jm.set_node_background_image(nodeid, image, width, height)` 方法可设置指定节点的背景图片。
+
+3.6. 获取数据
+===
+
+**获取元数据** : 使用 `jm.get_meta()` 方法可获取当前思维导图的元数据。
+
+**获取数据** : 使用 `jm.get_data(data_format)` 方法可获取当前思维导图的指定格式的数据文本。
+
+3.7. 其它操作
+===
+
+**清除节点的选中** : 使用 `jm.select_clear()` 方法可以清除当前的选中状态。
+
+**判断节点是否可见** : 使用 `jm.is_node_visible(node)` 方法可以判断此节点是否显示。
+
+版权声明
+===
+
+禁止转载、禁止演绎。
+
+jsMind 项目仍在不断升级变化,版本更新时会同时更新对应的文档。为避免给使用者带来困惑,在没有得到书面许可前,禁止转载本文档,同时禁止对本文档进行任何形式的更改。

+ 34 - 0
jsmind/docs/zh/4.contribution.md

@@ -0,0 +1,34 @@
+[目录](index.md)
+
+1. [基本用法](1.usage.md)
+2. [选项](2.options.md)
+3. [界面操控](3.operation.md)
+4. **参与贡献**
+
+4.1. 贡献代码
+===
+
+jsMind 可能在某些方面并不能满足您项目的需要,非常欢迎你对 jsMind 进行扩展,并通过贡献代码的方式进行反馈。
+
+贡献代码最方便的方式是给 jsMind 项目提交 pull-request,具体做法可参考 github 的帮助文档:
+
+* [Creating a pull request from a fork](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork)
+* [Working with forks](https://help.github.com/en/articles/working-with-forks)
+
+在提交 pull-request 前,请务必做好测试,并在提交 pull-request 时做出详细的说明,包括功能特性介绍、代码变更说明。
+
+4.2. 贡献想法或提出需求
+===
+
+或者您可以将您遇到的问题或需求通过 issue 的方式提出来供大家讨论,希望有能力和精力的朋友共同参与,一起解决问题。
+
+对于共性的需求,建议实现后回馈到开源项目中,对于非共性的需求,可fork出独立仓库进行定制开发。
+
+需要注意的是,开源并不意味着免费。 jsMind 本身并不限制商业使用,但为定制开发支付报酬也理所应当。
+
+版权声明
+===
+
+禁止转载、禁止演绎。
+
+jsMind 项目仍在不断升级变化,版本更新时会同时更新对应的文档。为避免给使用者带来困惑,在没有得到书面许可前,禁止转载本文档,同时禁止对本文档进行任何形式的更改。

+ 28 - 0
jsmind/docs/zh/index.md

@@ -0,0 +1,28 @@
+目录
+======
+
+* [1. 基本用法](1.usage.md)
+  * 1.1. 基本框架
+  * 1.2. 数据格式
+  * 1.3. 主题
+* [2. 选项](2.options.md)
+  * 2.1. 综述
+  * 2.2. 常规选项
+  * 2.3. 布局选项
+  * 2.4. 快捷键
+* [3. 界面操控](3.operation.md)
+  * 3.1. 显示思维导图
+  * 3.2. 查找节点
+  * 3.3. 操作节点
+  * 3.4. 编辑节点
+  * 3.5. 设置样式
+  * 3.6. 获取数据
+  * 3.7. 其它操作
+* [4. 参与贡献](4.contribution.md)
+
+版权声明
+======
+
+禁止转载、禁止演绎。
+
+jsMind 项目仍在不断升级变化,版本更新时会同时更新对应的文档。为避免给使用者带来困惑,在没有得到书面许可前,禁止转载本文档,同时禁止对本文档进行任何形式的更改。

+ 62 - 0
jsmind/example/1_basic.html

@@ -0,0 +1,62 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>jsMind</title>
+    <link type="text/css" rel="stylesheet" href="../style/jsmind.css" />
+    <style type="text/css">
+        #jsmind_container{
+            width:800px;
+            height:500px;
+            border:solid 1px #ccc;
+            /*background:#f4f4f4;*/
+            background:#f4f4f4;
+        }
+    </style>
+</head>
+<body>
+<div id="jsmind_container"></div>
+<script type="text/javascript" src="../js/jsmind.js"></script>
+<script type="text/javascript" src="../js/jsmind.draggable.js"></script>
+<script type="text/javascript">
+    function load_jsmind(){
+        var mind = {
+            "meta":{
+                "name":"demo",
+                "author":"hizzgdev@163.com",
+                "version":"0.2",
+            },
+            "format":"node_array",
+            "data":[
+                {"id":"root", "isroot":true, "topic":"jsMind"},
+
+                {"id":"sub1", "parentid":"root", "topic":"sub1", "background-color":"#0000ff"},
+                {"id":"sub11", "parentid":"sub1", "topic":"sub11"},
+                {"id":"sub12", "parentid":"sub1", "topic":"sub12"},
+                {"id":"sub13", "parentid":"sub1", "topic":"sub13"},
+
+                {"id":"sub2", "parentid":"root", "topic":"sub2"},
+                {"id":"sub21", "parentid":"sub2", "topic":"sub21"},
+                {"id":"sub22", "parentid":"sub2", "topic":"sub22","foreground-color":"#33ff33"},
+
+                {"id":"sub3", "parentid":"root", "topic":"sub3"},
+            ]
+        };
+        var options = {
+            container:'jsmind_container',
+            editable:true,
+            theme:'primary'
+        }
+        var jm = jsMind.show(options,mind);
+        // jm.set_readonly(true);
+        // var mind_data = jm.get_data();
+        // alert(mind_data);
+        jm.add_node("sub2","sub23", "new node", {"background-color":"red"});
+        jm.set_node_color('sub21', 'green', '#ccc');
+    }
+
+    load_jsmind();
+</script>
+</body>
+</html>

+ 526 - 0
jsmind/example/2_features.html

@@ -0,0 +1,526 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>jsMind</title>
+    <link type="text/css" rel="stylesheet" href="../style/jsmind.css" />
+    <style type="text/css">
+        li{margin-top:2px; margin-bottom:2px;}
+        button{width:140px;}
+        select{width:140px;}
+        #layout{width:1230px;}
+        #jsmind_nav{width:210px;height:600px;border:solid 1px #ccc;overflow:auto;float:left;}
+        .file_input{width:100px;}
+        button.sub{width:100px;}
+
+        #jsmind_container{
+            float:left;
+            width:1000px;
+            height:600px;
+            border:solid 1px #ccc;
+            background:#f4f4f4;
+        }
+    </style>
+</head>
+<body>
+<div id="layout">
+    <div id="jsmind_nav">
+        <div>1. Open</div>
+        <ol type='A'>
+            <li><button onclick="open_json();">open example</button></li>
+            <li><button onclick="open_ajax();">open remote</button></li>
+            <li><button onclick="prompt_info('see 6.Multi Format');">open local file</button></li>
+            <li><button onclick="prompt_info('see 6.Multi Format');">save local file</button></li>
+            <li><button onclick="screen_shot();">screenshot</button></li>
+        </ol>
+        </ol>
+        <div>2. Select &amp; Toggle</div>
+        <ol type='A'>
+            <li><button onclick="select_node();">select a node</button></li>
+            <li><button onclick="prompt_info('please try click a node');">try click a node</button></li>
+            <li><button onclick="show_selected();">get the selected</button></li>
+        </ol>
+        <div>3. Edit</div>
+        <ol type='A'>
+            <li><button onclick="toggle_editable(this);">disable editable</button></li>
+            <li><button onclick="add_node();">add a node</button></li>
+            <li><button onclick="add_image_node();">add a image node</button></li>
+            <li><button onclick="modify_node();">modify node</button></li>
+            <li><button onclick="prompt_info('please try double click a node');">try double click</button></li>
+            <li><button onclick="move_node();">move a node</button></li>
+            <li><button onclick="move_to_first();">move to first</button></li>
+            <li><button onclick="move_to_last();">move to last</button></li>
+            <li><button onclick="remove_node();">remove node</button></li>
+        </ol>
+        <div>4. Style</div>
+        <ol type='A'>
+            <li><button onclick="change_text_font();">change font</button></li>
+            <li><button onclick="change_text_color();">change color</button></li>
+            <li><button onclick="change_background_color();">change bg-color</button></li>
+            <li><button onclick="change_background_image();">change background</button></li>
+        </ol>
+        <div>5. Theme</div>
+        <ol type='A'>
+        <li>
+        <select onchange="set_theme(this.value);">
+            <option value="">default</option>
+            <option value="primary">primary</option>
+            <option value="warning">warning</option>
+            <option value="danger">danger</option>
+            <option value="success">success</option>
+            <option value="info">info</option>
+            <option value="greensea" selected="selected">greensea</option>
+            <option value="nephrite">nephrite</option>
+            <option value="belizehole">belizehole</option>
+            <option value="wisteria">wisteria</option>
+            <option value="asphalt">asphalt</option>
+            <option value="orange">orange</option>
+            <option value="pumpkin">pumpkin</option>
+            <option value="pomegranate">pomegranate</option>
+            <option value="clouds">clouds</option>
+            <option value="asbestos">asbestos</option>
+        </select>
+        </li>
+        </ol>
+        <div>6. Adjusting</div>
+        <ol type='A'>
+            <li><button onclick="change_container();">resize container</button>
+            <button onclick="resize_jsmind();">adusting</button></li>
+            <li>expand/collapse</li>
+            <ol>
+                <li><button class="sub" onclick="expand();">expand node</button></li>
+                <li><button class="sub" onclick="collapse();">collapse node</button></li>
+                <li><button class="sub" onclick="toggle();">toggle node</button></li>
+                <li><button class="sub" onclick="expand_to_level2();">expand to level 2</button></li>
+                <li><button class="sub" onclick="expand_to_level3();">expand to level 3</button></li>
+                <li><button class="sub" onclick="expand_all();">expand all</button></li>
+                <li><button class="sub" onclick="collapse_all();">collapse all</button></li>
+            </ol>
+            <li>zoom</li>
+
+            <button id="zoom-in-button" style="width:50px" onclick="zoomIn();">
+                In
+            </button>
+            <button id="zoom-out-button" style="width:50px" onclick="zoomOut();">
+                Out
+            </button>
+        </ol>
+
+        <div>7. Multi Format</div>
+        <ol type='A'>
+            <li>node_tree(default)</li>
+            <ol>
+                <li><button class="sub" onclick="show_data();">show data</button></li>
+                <li><button class="sub" onclick="save_file();">save file</button></li>
+                <li><input id="file_input" class="file_input" type="file"/></li>
+                <li><button class="sub" onclick="open_file();">open file</button></li>
+            </ol>
+            <li>node_array</li>
+            <ol>
+                <li><button class="sub" onclick="get_nodearray_data();">show data</button></li>
+                <li><button class="sub" onclick="save_nodearray_file();">save file</button></li>
+                <li><input id="file_input_nodearray" class="file_input" type="file"/></li>
+                <li><button class="sub" onclick="open_nodearray();">open file</button></li>
+            </ol>
+            <li>freemind(.mm)</li>
+            <ol>
+                <li><button class="sub" onclick="get_freemind_data();">show data</button></li>
+                <li><button class="sub" onclick="save_freemind_file();">save file</button></li>
+                <li><input id="file_input_freemind" class="file_input" type="file"/></li>
+                <li><button class="sub" onclick="open_freemind();">open file</button></li>
+            </ol>
+        </ol>
+    </div>
+    <div id="jsmind_container"></div>
+    <div style="display:none">
+      <input class="file" type="file" id="image-chooser" accept="image/*"/>
+    </div>
+
+</div>
+<script type="text/javascript" src="../js/jsmind.js"></script>
+<script type="text/javascript" src="../js/jsmind.draggable.js"></script>
+<script type="text/javascript" src="../js/jsmind.screenshot.js"></script>
+<script type="text/javascript">
+    var _jm = null;
+    function open_empty(){
+        var options = {
+            container:'jsmind_container',
+            theme:'greensea',
+            editable:true
+        }
+        _jm = jsMind.show(options);
+        // _jm = jsMind.show(options,mind);
+    }
+
+    function open_json(){
+        var mind = {
+            "meta":{
+                "name":"jsMind remote",
+                "author":"hizzgdev@163.com",
+                "version":"0.2"
+            },
+            "format":"node_tree",
+            "data":{"id":"root","topic":"jsMind","children":[
+                {"id":"easy","topic":"Easy","direction":"left","children":[
+                    {"id":"easy1","topic":"Easy to show"},
+                    {"id":"easy2","topic":"Easy to edit"},
+                    {"id":"easy3","topic":"Easy to store"},
+                    {"id":"easy4","topic":"Easy to embed"},
+                    {"id":"other3","background-image":"ant.png", "width": "100", "height": "100"}
+                ]},
+                {"id":"open","topic":"Open Source","direction":"right","children":[
+                    {"id":"open1","topic":"on GitHub", "background-color":"#eee", "foreground-color":"blue"},
+                    {"id":"open2","topic":"BSD License"}
+                ]},
+                {"id":"powerful","topic":"Powerful","direction":"right","children":[
+                    {"id":"powerful1","topic":"Base on Javascript"},
+                    {"id":"powerful2","topic":"Base on HTML5"},
+                    {"id":"powerful3","topic":"Depends on you"}
+                ]},
+                {"id":"other","topic":"test node","direction":"left","children":[
+                    {"id":"other1","topic":"I'm from local variable"},
+                    {"id":"other2","topic":"I can do everything"}
+                ]}
+            ]}
+        }
+        _jm.show(mind);
+    }
+
+    function open_ajax(){
+        var mind_url = 'data_example.json';
+        jsMind.util.ajax.get(mind_url,function(mind){
+            _jm.show(mind);
+        });
+    }
+
+    function screen_shot(){
+        _jm.screenshot.shootDownload();
+    }
+
+    function show_data(){
+        var mind_data = _jm.get_data();
+        var mind_string = jsMind.util.json.json2string(mind_data);
+        prompt_info(mind_string);
+    }
+
+    function save_file(){
+        var mind_data = _jm.get_data();
+        var mind_name = mind_data.meta.name;
+        var mind_str = jsMind.util.json.json2string(mind_data);
+        jsMind.util.file.save(mind_str,'text/jsmind',mind_name+'.jm');
+    }
+    
+    function open_file(){
+        var file_input = document.getElementById('file_input');
+        var files = file_input.files;
+        if(files.length > 0){
+            var file_data = files[0];
+            jsMind.util.file.read(file_data,function(jsmind_data, jsmind_name){
+                var mind = jsMind.util.json.string2json(jsmind_data);
+                if(!!mind){
+                    _jm.show(mind);
+                }else{
+                    prompt_info('can not open this file as mindmap');
+                }
+            });
+        }else{
+            prompt_info('please choose a file first')
+        }
+    }
+
+    function select_node(){
+        var nodeid = 'other';
+        _jm.select_node(nodeid);
+    }
+
+    function show_selected(){
+        var selected_node = _jm.get_selected_node();
+        if(!!selected_node){
+            prompt_info(selected_node.topic);
+        }else{
+            prompt_info('nothing');
+        }
+    }
+
+    function get_selected_nodeid(){
+        var selected_node = _jm.get_selected_node();
+        if(!!selected_node){
+            return selected_node.id;
+        }else{
+            return null;
+        }
+    }
+
+    function add_node(){
+        var selected_node = _jm.get_selected_node(); // as parent of new node
+        if(!selected_node){prompt_info('please select a node first.');return;}
+
+        var nodeid = jsMind.util.uuid.newid();
+        var topic = '* Node_'+nodeid.substr(nodeid.length-6)+' *';
+        var node = _jm.add_node(selected_node, nodeid, topic);
+    }
+
+    var imageChooser = document.getElementById('image-chooser');
+
+    imageChooser.addEventListener('change', function (event) {
+        // Read file here.
+        var reader = new FileReader();
+        reader.onloadend = (function () {
+            var selected_node = _jm.get_selected_node();
+            var nodeid = jsMind.util.uuid.newid();
+            var topic = undefined;
+            var data = {
+                "background-image": reader.result,
+                "width": "100",
+                "height": "100"};
+            var node = _jm.add_node(selected_node, nodeid, topic, data);
+            //var node = _jm.add_image_node(selected_node, nodeid, reader.result, 100, 100);
+        //add_image_node:function(parent_node, nodeid, image, width, height, data, idx, direction, expanded){
+        });
+
+        var file = imageChooser.files[0];
+        if (file) {
+            reader.readAsDataURL(file);
+        };
+
+    }, false);
+
+    function add_image_node(){
+        var selected_node = _jm.get_selected_node(); // as parent of new node
+        if(!selected_node){
+            prompt_info('please select a node first.');
+            return;
+        }
+
+        imageChooser.focus();
+        imageChooser.click();
+    }
+
+    function modify_node(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        // modify the topic
+        _jm.update_node(selected_id, '--- modified ---');
+    }
+
+    function move_to_first(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.move_node(selected_id,'_first_');
+    }
+
+    function move_to_last(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.move_node(selected_id,'_last_');
+    }
+
+    function move_node(){
+        // move a node before another
+        _jm.move_node('other','open');
+    }
+
+    function remove_node(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.remove_node(selected_id);
+    }
+
+    function change_text_font(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.set_node_font_style(selected_id, 28);
+    }
+
+    function change_text_color(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.set_node_color(selected_id, null, '#000');
+    }
+
+    function change_background_color(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.set_node_color(selected_id, '#eee', null);
+    }
+
+    function change_background_image(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.set_node_background_image(selected_id, 'ant.png', 100, 100);
+    }
+
+    function set_theme(theme_name){
+        _jm.set_theme(theme_name);
+    }
+
+    var zoomInButton = document.getElementById("zoom-in-button");
+    var zoomOutButton = document.getElementById("zoom-out-button");
+
+    function zoomIn() {
+        if (_jm.view.zoomIn()) {
+            zoomOutButton.disabled = false;
+        } else {
+            zoomInButton.disabled = true;
+        };
+    };
+
+    function zoomOut() {
+        if (_jm.view.zoomOut()) {
+            zoomInButton.disabled = false;
+        } else {
+            zoomOutButton.disabled = true;
+        };
+    };
+
+    function toggle_editable(btn){
+        var editable = _jm.get_editable();
+        if(editable){
+            _jm.disable_edit();
+            btn.innerHTML = 'enable editable';
+        }else{
+            _jm.enable_edit();
+            btn.innerHTML = 'disable editable';
+        }
+    }
+
+    // this method change size of container, perpare for adjusting jsmind
+    function change_container(){
+        var c = document.getElementById('jsmind_container');
+        c.style.width = '800px';
+        c.style.height = '500px';
+    }
+
+    function resize_jsmind(){
+        _jm.resize();
+    }
+
+    function expand(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.expand_node(selected_id);
+    }
+
+    function collapse(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.collapse_node(selected_id);
+    }
+
+    function toggle(){
+        var selected_id = get_selected_nodeid();
+        if(!selected_id){prompt_info('please select a node first.');return;}
+
+        _jm.toggle_node(selected_id);
+    }
+
+    function expand_all(){
+        _jm.expand_all();
+    }
+
+    function expand_to_level2(){
+        _jm.expand_to_depth(2);
+    }
+
+    function expand_to_level3(){
+        _jm.expand_to_depth(3);
+    }
+
+    function collapse_all(){
+        _jm.collapse_all();
+    }
+
+
+    function get_nodearray_data(){
+        var mind_data = _jm.get_data('node_array');
+        var mind_string = jsMind.util.json.json2string(mind_data);
+        prompt_info(mind_string);
+    }
+
+    function save_nodearray_file(){
+        var mind_data = _jm.get_data('node_array');
+        var mind_name = mind_data.meta.name;
+        var mind_str = jsMind.util.json.json2string(mind_data);
+        jsMind.util.file.save(mind_str,'text/jsmind',mind_name+'.jm');
+    }
+    
+    function open_nodearray(){
+        var file_input = document.getElementById('file_input_nodearray');
+        var files = file_input.files;
+        if(files.length > 0){
+            var file_data = files[0];
+            jsMind.util.file.read(file_data,function(jsmind_data, jsmind_name){
+                var mind = jsMind.util.json.string2json(jsmind_data);
+                if(!!mind){
+                    _jm.show(mind);
+                }else{
+                    prompt_info('can not open this file as mindmap');
+                }
+            });
+        }else{
+            prompt_info('please choose a file first')
+        }
+    }
+
+    function get_freemind_data(){
+        var mind_data = _jm.get_data('freemind');
+        var mind_string = jsMind.util.json.json2string(mind_data);
+        alert(mind_string);
+    }
+
+    function save_freemind_file(){
+        var mind_data = _jm.get_data('freemind');
+        var mind_name = mind_data.meta.name || 'freemind';
+        var mind_str = mind_data.data;
+        jsMind.util.file.save(mind_str,'text/xml',mind_name+'.mm');
+    }
+    
+    function open_freemind(){
+        var file_input = document.getElementById('file_input_freemind');
+        var files = file_input.files;
+        if(files.length > 0){
+            var file_data = files[0];
+            jsMind.util.file.read(file_data, function(freemind_data, freemind_name){
+                if(freemind_data){
+                    var mind_name = freemind_name;
+                    if(/.*\.mm$/.test(mind_name)){
+                        mind_name = freemind_name.substring(0,freemind_name.length-3);
+                    }
+                    var mind = {
+                        "meta":{
+                            "name":mind_name,
+                            "author":"hizzgdev@163.com",
+                            "version":"1.0.1"
+                        },
+                        "format":"freemind",
+                        "data":freemind_data
+                    };
+                    _jm.show(mind);
+                }else{
+                    prompt_info('can not open this file as mindmap');
+                }
+            });
+        }else{
+            prompt_info('please choose a file first')
+        }
+    }
+
+    function prompt_info(msg){
+        alert(msg);
+    }
+
+    open_empty();
+</script>
+</body>
+</html>

BIN
jsmind/example/ant.png


+ 48 - 0
jsmind/example/data_example.json

@@ -0,0 +1,48 @@
+{
+    "meta":{
+        "name":"jsMind remote",
+        "author":"hizzgdev@163.com",
+        "version":"0.2"
+    },
+    "format":"node_tree",
+    "data":{"id":"root","topic":"jsMind","children":[
+        {"id":"easy","topic":"Easy","direction":"left","expanded":false,"children":[
+            {"id":"easy1","topic":"Easy to show"},
+            {"id":"easy2","topic":"Easy to edit"},
+            {"id":"easy3","topic":"Easy to store"},
+            {"id":"easy4","topic":"Easy to embed","children":[
+                {"id":"easy41","topic":"Easy to show"},
+                {"id":"easy42","topic":"Easy to edit"},
+                {"id":"easy43","topic":"Easy to store"},
+                {"id":"open44","topic":"BSD License","children":[
+                    {"id":"open441","topic":"on GitHub"},
+                    {"id":"open442","topic":"BSD License"}
+                ]},
+                {"id":"easy45","topic":"Easy to embed"}
+            ]}
+        ]},
+        {"id":"open","topic":"Open Source","direction":"right","children":[
+            {"id":"open1","topic":"on GitHub"},
+            {"id":"open2","topic":"BSD License","children":[
+                {"id":"open21","topic":"on GitHub"},
+                {"id":"open22","topic":"BSD License","children":[
+                    {"id":"open221","topic":"on GitHub"},
+                    {"id":"open222","topic":"BSD License"}
+                ]}
+            ]}
+        ]},
+        {"id":"powerful","topic":"Powerful","direction":"right","expanded":false,"children":[
+            {"id":"powerful1","topic":"Base on Javascript"},
+            {"id":"powerful2","topic":"Base on HTML5"},
+            {"id":"powerful3","topic":"Depends on you","expanded":false,"children":[
+                {"id":"powerful31","topic":"Base on Javascript"},
+                {"id":"powerful32","topic":"Base on HTML5"},
+                {"id":"powerful33","topic":"Depends on you"}
+            ]}
+        ]},
+        {"id":"other","topic":"test node","direction":"left","children":[
+            {"id":"other1","topic":"I'm from ajax"},
+            {"id":"other2","topic":"I can do everything"}
+        ]}
+    ]}
+}

+ 102 - 0
jsmind/example/demo.html

@@ -0,0 +1,102 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>jsMind</title>
+    <link type="text/css" rel="stylesheet" href="../style/jsmind.css" />
+    <style type="text/css">
+        #jsmind_container{
+            width:800px;
+            height:500px;
+            border:solid 1px #ccc;
+            /*background:#f4f4f4;*/
+            background:#f4f4f4;
+        }
+    </style>
+</head>
+<body>
+<input type="file" onchange="load_file(this);"/>
+<button onclick="save_nodetree();">nodetree</button>
+<button onclick="replay();">replay</button>
+<div id="jsmind_container"></div>
+<script type="text/javascript" src="../js/jsmind.js"></script>
+<script type="text/javascript" src="../js/jsmind.draggable.js"></script>
+<script type="text/javascript" src="../features/jsmind.shell.js"></script>
+<script type="text/javascript">
+    var _jm = null;
+    function load_jsmind(){
+        var mind = {
+            meta:{
+                name:'demo',
+                author:'hizzgdev@163.com',
+                version:'0.2'
+            },
+            format:'node_array',
+            data:[
+                {"id":"root", "isroot":true, "topic":"jsMind"},
+
+                {"id":"sub1", "parentid":"root", "topic":"sub1"},
+                {"id":"sub11", "parentid":"sub1", "topic":"sub11"},
+                {"id":"sub12", "parentid":"sub1", "topic":"sub12"},
+                {"id":"sub13", "parentid":"sub1", "topic":"sub13"},
+
+                {"id":"sub2", "parentid":"root", "topic":"sub2"},
+                {"id":"sub21", "parentid":"sub2", "topic":"sub21"},
+                {"id":"sub22", "parentid":"sub2", "topic":"sub22"},
+
+                {"id":"sub3", "parentid":"root", "topic":"sub3"},
+            ]
+        };
+        var options = {
+            container:'jsmind_container',
+            editable:true,
+            theme:'primary',
+            shortcut:{
+                handles:{
+                    test:function(j,e){
+                        console.log(j);
+                    }
+                },
+                mapping:{
+                    test:89
+                }
+            }
+        }
+        _jm = jsMind.show(options,mind);
+        // jm.set_readonly(true);
+        // var mind_data = jm.get_data();
+        // alert(mind_data);
+    }
+    
+    function load_file(fi){
+        var files = fi.files;
+        if(files.length > 0){
+            var file_data = files[0];
+            jsMind.util.file.read(file_data, function(freemind_data, jsmind_name){
+                var mind = jsmind_data;
+                if(!!mind){
+                    _jm.show(mind);
+                }else{
+                    console.error('can not open this file as mindmap');
+                }
+            });
+        }
+    }
+
+    function save_nodetree(){
+        var mind_data = _jm.get_data('node_tree');
+        console.log(mind_data);
+    }
+
+    function replay(){
+        var shell = _jm.shell;
+        if(!!shell){
+            shell.replay();
+        }
+    }
+
+    load_jsmind();
+</script>
+</body>
+</html>

+ 116 - 0
jsmind/features/jsmind.shell.js

@@ -0,0 +1,116 @@
+/*
+ * Released under BSD License
+ * Copyright (c) 2014-2021 hizzgdev@163.com
+ * 
+ * Project Home:
+ *   https://github.com/hizzgdev/jsmind/
+ */
+
+(function ($w) {
+    'use strict';
+    var $d = $w.document;
+    var __name__ = 'jsMind';
+    var jsMind = $w[__name__];
+    if (!jsMind) { return; }
+    if (typeof (jsMind.shell) != 'undefined') { return; }
+
+    var options = {
+        play_delay: 1000
+    };
+
+    jsMind.shell = function (jm) {
+        this.jm = jm;
+        this.step = 0;
+        this.commands = []; //version
+        this.delay_handle = 0;
+        this.playing = false;
+        this.jm_editable = this.jm.get_editable();
+    };
+
+    jsMind.shell.prototype = {
+        init: function () {
+            this.playing = false;
+        },
+        record: function (action, obj) {
+            if (!this.playing) {
+                var command = { action: action, data: obj.data, node: obj.node };
+                var prev_command = this.commands[this.step - 1];
+                if (command.action === 'update_node' && prev_command.action === 'add_node' && prev_command.data[2] === 'New Node') {
+                    prev_command.data[2] = command.data[1];
+                    this.commands[this.step - 1] = prev_command;
+                } else {
+                    this.step = this.commands.push(command);
+                }
+            }
+        },
+        execute: function (command) {
+            var func = this.jm[command.action];
+            var node = command.node;
+            this.jm.enable_edit();
+            func.apply(this.jm, command.data);
+            this.jm.disable_edit();
+            if (!!node) {
+                this.jm.select_node(node);
+            }
+        },
+        add_command: function (command) {
+            this.commands.push(command);
+            play();
+        },
+        replay: function () {
+            this.step = 0;
+            this.play();
+        },
+        play: function () {
+            this.jm.disable_edit();
+            this.playing = true;
+            this._play_stepbystep();
+        },
+        _play_stepbystep: function () {
+            if (this.delay_handle != 0) {
+                $w.clearTimeout(this.delay_handle);
+                this.delay_handle = 0;
+            }
+            if (this.step < this.commands.length) {
+                this.execute(this.commands[this.step]);
+                this.step++;
+                var js = this;
+                this.delay_handle = $w.setTimeout(function () {
+                    js.play.call(js);
+                }, options.play_delay);
+            } else {
+                this._play_end();
+            }
+        },
+        _play_end: function () {
+            this.playing = false;
+            if (this.jm_editable) {
+                this.jm.enable_edit();
+            } else {
+                this.jm.disable_edit();
+            }
+        },
+
+        jm_event_handle: function (type, data) {
+            if (type === jsMind.event_type.show) {
+                this.record('show', data);
+            }
+            if (type === jsMind.event_type.edit) {
+                var action = data.evt;
+                delete data.evt;
+                this.record(action, data);
+            }
+        }
+    };
+
+    var shell_plugin = new jsMind.plugin('shell', function (jm) {
+        var js = new jsMind.shell(jm);
+        jm.shell = js;
+        js.init();
+        jm.add_event_listener(function (type, data) {
+            js.jm_event_handle.call(js, type, data);
+        });
+    });
+
+    jsMind.register_plugin(shell_plugin);
+})(window);

+ 353 - 0
jsmind/js/jsmind.draggable.js

@@ -0,0 +1,353 @@
+/*
+ * Released under BSD License
+ * Copyright (c) 2014-2021 hizzgdev@163.com
+ * 
+ * Project Home:
+ *   https://github.com/hizzgdev/jsmind/
+ */
+
+(function ($w) {
+    'use strict';
+    var $d = $w.document;
+    var __name__ = 'jsMind';
+    var jsMind = $w[__name__];
+    if (!jsMind) { return; }
+    if (typeof jsMind.draggable != 'undefined') { return; }
+
+    var jdom = jsMind.util.dom;
+    var clear_selection = 'getSelection' in $w ? function () {
+        $w.getSelection().removeAllRanges();
+    } : function () {
+        $d.selection.empty();
+    };
+
+    var options = {
+        line_width: 5,
+        lookup_delay: 500,
+        lookup_interval: 80
+    };
+
+    jsMind.draggable = function (jm) {
+        this.jm = jm;
+        this.e_canvas = null;
+        this.canvas_ctx = null;
+        this.shadow = null;
+        this.shadow_w = 0;
+        this.shadow_h = 0;
+        this.active_node = null;
+        this.target_node = null;
+        this.target_direct = null;
+        this.client_w = 0;
+        this.client_h = 0;
+        this.offset_x = 0;
+        this.offset_y = 0;
+        this.hlookup_delay = 0;
+        this.hlookup_timer = 0;
+        this.capture = false;
+        this.moved = false;
+    };
+
+    jsMind.draggable.prototype = {
+        init: function () {
+            this._create_canvas();
+            this._create_shadow();
+            this._event_bind();
+        },
+
+        resize: function () {
+            this.jm.view.e_nodes.appendChild(this.shadow);
+            this.e_canvas.width = this.jm.view.size.w;
+            this.e_canvas.height = this.jm.view.size.h;
+        },
+
+        _create_canvas: function () {
+            var c = $d.createElement('canvas');
+            this.jm.view.e_panel.appendChild(c);
+            var ctx = c.getContext('2d');
+            this.e_canvas = c;
+            this.canvas_ctx = ctx;
+        },
+
+        _create_shadow: function () {
+            var s = $d.createElement('jmnode');
+            s.style.visibility = 'hidden';
+            s.style.zIndex = '3';
+            s.style.cursor = 'move';
+            s.style.opacity = '0.7';
+            this.shadow = s;
+        },
+
+        reset_shadow: function (el) {
+            var s = this.shadow.style;
+            this.shadow.innerHTML = el.innerHTML;
+            s.left = el.style.left;
+            s.top = el.style.top;
+            s.width = el.style.width;
+            s.height = el.style.height;
+            s.backgroundImage = el.style.backgroundImage;
+            s.backgroundSize = el.style.backgroundSize;
+            s.transform = el.style.transform;
+            this.shadow_w = this.shadow.clientWidth;
+            this.shadow_h = this.shadow.clientHeight;
+
+        },
+
+        show_shadow: function () {
+            if (!this.moved) {
+                this.shadow.style.visibility = 'visible';
+            }
+        },
+
+        hide_shadow: function () {
+            this.shadow.style.visibility = 'hidden';
+        },
+
+        _magnet_shadow: function (node) {
+            if (!!node) {
+                this.canvas_ctx.lineWidth = options.line_width;
+                this.canvas_ctx.strokeStyle = 'rgba(0,0,0,0.3)';
+                this.canvas_ctx.lineCap = 'round';
+                this._clear_lines();
+                this._canvas_lineto(node.sp.x, node.sp.y, node.np.x, node.np.y);
+            }
+        },
+
+        _clear_lines: function () {
+            this.canvas_ctx.clearRect(0, 0, this.jm.view.size.w, this.jm.view.size.h);
+        },
+
+        _canvas_lineto: function (x1, y1, x2, y2) {
+            this.canvas_ctx.beginPath();
+            this.canvas_ctx.moveTo(x1, y1);
+            this.canvas_ctx.lineTo(x2, y2);
+            this.canvas_ctx.stroke();
+        },
+
+        _lookup_close_node: function () {
+            var root = this.jm.get_root();
+            var root_location = root.get_location();
+            var root_size = root.get_size();
+            var root_x = root_location.x + root_size.w / 2;
+
+            var sw = this.shadow_w;
+            var sh = this.shadow_h;
+            var sx = this.shadow.offsetLeft;
+            var sy = this.shadow.offsetTop;
+
+            var ns, nl;
+
+            var direct = (sx + sw / 2) >= root_x ?
+                jsMind.direction.right : jsMind.direction.left;
+            var nodes = this.jm.mind.nodes;
+            var node = null;
+            var layout = this.jm.layout;
+            var min_distance = Number.MAX_VALUE;
+            var distance = 0;
+            var closest_node = null;
+            var closest_p = null;
+            var shadow_p = null;
+            for (var nodeid in nodes) {
+                var np, sp;
+                node = nodes[nodeid];
+                if (node.isroot || node.direction == direct) {
+                    if (node.id == this.active_node.id) {
+                        continue;
+                    }
+                    if (!layout.is_visible(node)) {
+                        continue;
+                    }
+                    ns = node.get_size();
+                    nl = node.get_location();
+                    if (direct == jsMind.direction.right) {
+                        if (sx - nl.x - ns.w <= 0) { continue; }
+                        distance = Math.abs(sx - nl.x - ns.w) + Math.abs(sy + sh / 2 - nl.y - ns.h / 2);
+                        np = { x: nl.x + ns.w - options.line_width, y: nl.y + ns.h / 2 };
+                        sp = { x: sx + options.line_width, y: sy + sh / 2 };
+                    } else {
+                        if (nl.x - sx - sw <= 0) { continue; }
+                        distance = Math.abs(sx + sw - nl.x) + Math.abs(sy + sh / 2 - nl.y - ns.h / 2);
+                        np = { x: nl.x + options.line_width, y: nl.y + ns.h / 2 };
+                        sp = { x: sx + sw - options.line_width, y: sy + sh / 2 };
+                    }
+                    if (distance < min_distance) {
+                        closest_node = node;
+                        closest_p = np;
+                        shadow_p = sp;
+                        min_distance = distance;
+                    }
+                }
+            }
+            var result_node = null;
+            if (!!closest_node) {
+                result_node = {
+                    node: closest_node,
+                    direction: direct,
+                    sp: shadow_p,
+                    np: closest_p
+                };
+            }
+            return result_node;
+        },
+
+        lookup_close_node: function () {
+            var node_data = this._lookup_close_node();
+            if (!!node_data) {
+                this._magnet_shadow(node_data);
+                this.target_node = node_data.node;
+                this.target_direct = node_data.direction;
+            }
+        },
+
+        _event_bind: function () {
+            var jd = this;
+            var container = this.jm.view.container;
+            jdom.add_event(container, 'mousedown', function (e) {
+                var evt = e || event;
+                jd.dragstart.call(jd, evt);
+            });
+            jdom.add_event(container, 'mousemove', function (e) {
+                var evt = e || event;
+                jd.drag.call(jd, evt);
+            });
+            jdom.add_event(container, 'mouseup', function (e) {
+                var evt = e || event;
+                jd.dragend.call(jd, evt);
+            });
+            jdom.add_event(container, 'touchstart', function (e) {
+                var evt = e || event;
+                jd.dragstart.call(jd, evt);
+            });
+            jdom.add_event(container, 'touchmove', function (e) {
+                var evt = e || event;
+                jd.drag.call(jd, evt);
+            });
+            jdom.add_event(container, 'touchend', function (e) {
+                var evt = e || event;
+                jd.dragend.call(jd, evt);
+            });
+        },
+
+        dragstart: function (e) {
+            if (!this.jm.get_editable()) { return; }
+            if (this.capture) { return; }
+            this.active_node = null;
+
+            var jview = this.jm.view;
+            var el = e.target || event.srcElement;
+            if (el.tagName.toLowerCase() != 'jmnode') { return; }
+            var nodeid = jview.get_binded_nodeid(el);
+            if (!!nodeid) {
+                var node = this.jm.get_node(nodeid);
+                if (!node.isroot) {
+                    this.reset_shadow(el);
+                    this.active_node = node;
+                    this.offset_x = (e.clientX || e.touches[0].clientX) / jview.actualZoom - el.offsetLeft;
+                    this.offset_y = (e.clientY || e.touches[0].clientY) / jview.actualZoom - el.offsetTop;
+                    this.client_hw = Math.floor(el.clientWidth / 2);
+                    this.client_hh = Math.floor(el.clientHeight / 2);
+                    if (this.hlookup_delay != 0) {
+                        $w.clearTimeout(this.hlookup_delay);
+                    }
+                    if (this.hlookup_timer != 0) {
+                        $w.clearInterval(this.hlookup_timer);
+                    }
+                    var jd = this;
+                    this.hlookup_delay = $w.setTimeout(function () {
+                        jd.hlookup_delay = 0;
+                        jd.hlookup_timer = $w.setInterval(function () {
+                            jd.lookup_close_node.call(jd);
+                        }, options.lookup_interval);
+                    }, options.lookup_delay);
+                    this.capture = true;
+                }
+            }
+        },
+
+        drag: function (e) {
+            if (!this.jm.get_editable()) { return; }
+            if (this.capture) {
+                e.preventDefault();
+                this.show_shadow();
+                this.moved = true;
+                clear_selection();
+                var jview = this.jm.view;
+                var px = (e.clientX || e.touches[0].clientX) / jview.actualZoom - this.offset_x;
+                var py = (e.clientY || e.touches[0].clientY) / jview.actualZoom - this.offset_y;
+                this.shadow.style.left = px + 'px';
+                this.shadow.style.top = py + 'px';
+                clear_selection();
+            }
+        },
+
+        dragend: function (e) {
+            if (!this.jm.get_editable()) { return; }
+            if (this.capture) {
+                if (this.hlookup_delay != 0) {
+                    $w.clearTimeout(this.hlookup_delay);
+                    this.hlookup_delay = 0;
+                    this._clear_lines();
+                }
+                if (this.hlookup_timer != 0) {
+                    $w.clearInterval(this.hlookup_timer);
+                    this.hlookup_timer = 0;
+                    this._clear_lines();
+                }
+                if (this.moved) {
+                    var src_node = this.active_node;
+                    var target_node = this.target_node;
+                    var target_direct = this.target_direct;
+                    this.move_node(src_node, target_node, target_direct);
+                }
+                this.hide_shadow();
+            }
+            this.moved = false;
+            this.capture = false;
+        },
+
+        move_node: function (src_node, target_node, target_direct) {
+            var shadow_h = this.shadow.offsetTop;
+            if (!!target_node && !!src_node && !jsMind.node.inherited(src_node, target_node)) {
+                // lookup before_node
+                var sibling_nodes = target_node.children;
+                var sc = sibling_nodes.length;
+                var node = null;
+                var delta_y = Number.MAX_VALUE;
+                var node_before = null;
+                var beforeid = '_last_';
+                while (sc--) {
+                    node = sibling_nodes[sc];
+                    if (node.direction == target_direct && node.id != src_node.id) {
+                        var dy = node.get_location().y - shadow_h;
+                        if (dy > 0 && dy < delta_y) {
+                            delta_y = dy;
+                            node_before = node;
+                            beforeid = '_first_';
+                        }
+                    }
+                }
+                if (!!node_before) { beforeid = node_before.id; }
+                this.jm.move_node(src_node.id, beforeid, target_node.id, target_direct);
+            }
+            this.active_node = null;
+            this.target_node = null;
+            this.target_direct = null;
+        },
+
+        jm_event_handle: function (type, data) {
+            if (type === jsMind.event_type.resize) {
+                this.resize();
+            }
+        }
+    };
+
+    var draggable_plugin = new jsMind.plugin('draggable', function (jm) {
+        var jd = new jsMind.draggable(jm);
+        jd.init();
+        jm.add_event_listener(function (type, data) {
+            jd.jm_event_handle.call(jd, type, data);
+        });
+    });
+
+    jsMind.register_plugin(draggable_plugin);
+
+})(window);

+ 3027 - 0
jsmind/js/jsmind.js

@@ -0,0 +1,3027 @@
+/*
+ * Released under BSD License
+ * Copyright (c) 2014-2021 hizzgdev@163.com
+ *
+ * Project Home:
+ *   https://github.com/hizzgdev/jsmind/
+ */
+
+; (function ($w) {
+    'use strict';
+    // set 'jsMind' as the library name.
+    // __name__ should be a const value, Never try to change it easily.
+    var __name__ = 'jsMind';
+    // library version
+    var __version__ = '0.4.6';
+    // author
+    var __author__ = 'hizzgdev@163.com';
+
+    // an noop function define
+    var _noop = function () { };
+    var logger = (typeof console === 'undefined') ? {
+        log: _noop, debug: _noop, error: _noop, warn: _noop, info: _noop
+    } : console;
+
+    // check global variables
+    if (typeof module === 'undefined' || !module.exports) {
+        if (typeof $w[__name__] != 'undefined') {
+            logger.log(__name__ + ' has been already exist.');
+            return;
+        }
+    }
+
+    // shortcut of methods in dom
+    var $d = $w.document;
+    var $g = function (id) { return $d.getElementById(id); };
+    var $c = function (tag) { return $d.createElement(tag); };
+    var $t = function (n, t) { if (n.hasChildNodes()) { n.firstChild.nodeValue = t; } else { n.appendChild($d.createTextNode(t)); } };
+
+    var $h = function (n, t) {
+        if (t instanceof HTMLElement) {
+            n.innerHTML = '';
+            n.appendChild(t);
+        } else {
+            n.innerHTML = t;
+        }
+    };
+    // detect isElement
+    var $i = function (el) { return !!el && (typeof el === 'object') && (el.nodeType === 1) && (typeof el.style === 'object') && (typeof el.ownerDocument === 'object'); };
+    if (typeof String.prototype.startsWith != 'function') { String.prototype.startsWith = function (p) { return this.slice(0, p.length) === p; }; }
+
+    var DEFAULT_OPTIONS = {
+        container: '',   // id of the container
+        editable: false, // you can change it in your options
+        theme: null,
+        mode: 'full',    // full or side
+        support_html: true,
+
+        view: {
+            engine: 'canvas',
+            hmargin: 100,
+            vmargin: 50,
+            line_width: 2,
+            line_color: '#555'
+        },
+        layout: {
+            hspace: 30,
+            vspace: 20,
+            pspace: 13
+        },
+        default_event_handle: {
+            enable_mousedown_handle: true,
+            enable_click_handle: true,
+            enable_dblclick_handle: true
+        },
+        shortcut: {
+            enable: true,
+            handles: {
+            },
+            mapping: {
+                addchild: 45, // Insert
+                addbrother: 13, // Enter
+                editnode: 113,// F2
+                delnode: 46, // Delete
+                toggle: 32, // Space
+                left: 37, // Left
+                up: 38, // Up
+                right: 39, // Right
+                down: 40, // Down
+            }
+        },
+    };
+
+    // core object
+    var jm = function (options) {
+        jm.current = this;
+
+        this.version = __version__;
+        var opts = {};
+        jm.util.json.merge(opts, DEFAULT_OPTIONS);
+        jm.util.json.merge(opts, options);
+
+        if (!opts.container) {
+            logger.error('the options.container should not be null or empty.');
+            return;
+        }
+        this.options = opts;
+        this.inited = false;
+        this.mind = null;
+        this.event_handles = [];
+        this.init();
+    };
+
+    // ============= static object =============================================
+    jm.direction = { left: -1, center: 0, right: 1 };
+    jm.event_type = { show: 1, resize: 2, edit: 3, select: 4 };
+    jm.key = { meta: 1 << 13, ctrl: 1 << 12, alt: 1 << 11, shift: 1 << 10 };
+
+    jm.node = function (sId, iIndex, sTopic, oData, bIsRoot, oParent, eDirection, bExpanded) {
+        if (!sId) { logger.error('invalid nodeid'); return; }
+        if (typeof iIndex != 'number') { logger.error('invalid node index'); return; }
+        if (typeof bExpanded === 'undefined') { bExpanded = true; }
+        this.id = sId;
+        this.index = iIndex;
+        this.topic = sTopic;
+        this.data = oData || {};
+        this.isroot = bIsRoot;
+        this.parent = oParent;
+        this.direction = eDirection;
+        this.expanded = !!bExpanded;
+        this.children = [];
+        this._data = {};
+    };
+
+    jm.node.compare = function (node1, node2) {
+        // '-1' is alwary the last
+        var r = 0;
+        var i1 = node1.index;
+        var i2 = node2.index;
+        if (i1 >= 0 && i2 >= 0) {
+            r = i1 - i2;
+        } else if (i1 == -1 && i2 == -1) {
+            r = 0;
+        } else if (i1 == -1) {
+            r = 1;
+        } else if (i2 == -1) {
+            r = -1;
+        } else {
+            r = 0;
+        }
+        //logger.debug(i1+' <> '+i2+'  =  '+r);
+        return r;
+    };
+
+    jm.node.inherited = function (pnode, node) {
+        if (!!pnode && !!node) {
+            if (pnode.id === node.id) {
+                return true;
+            }
+            if (pnode.isroot) {
+                return true;
+            }
+            var pid = pnode.id;
+            var p = node;
+            while (!p.isroot) {
+                p = p.parent;
+                if (p.id === pid) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    };
+
+    jm.node.prototype = {
+        get_location: function () {
+            var vd = this._data.view;
+            return {
+                x: vd.abs_x,
+                y: vd.abs_y
+            };
+        },
+        get_size: function () {
+            var vd = this._data.view;
+            return {
+                w: vd.width,
+                h: vd.height
+            }
+        }
+    };
+
+
+    jm.mind = function () {
+        this.name = null;
+        this.author = null;
+        this.version = null;
+        this.root = null;
+        this.selected = null;
+        this.nodes = {};
+    };
+
+    jm.mind.prototype = {
+        get_node: function (nodeid) {
+            if (nodeid in this.nodes) {
+                return this.nodes[nodeid];
+            } else {
+                logger.warn('the node[id=' + nodeid + '] can not be found');
+                return null;
+            }
+        },
+
+        set_root: function (nodeid, topic, data) {
+            if (this.root == null) {
+                this.root = new jm.node(nodeid, 0, topic, data, true);
+                this._put_node(this.root);
+            } else {
+                logger.error('root node is already exist');
+            }
+        },
+
+        add_node: function (parent_node, nodeid, topic, data, idx, direction, expanded) {
+            if (!jm.util.is_node(parent_node)) {
+                var the_parent_node = this.get_node(parent_node);
+                if (!the_parent_node) {
+                    logger.error('the parent_node[id=' + parent_node + '] can not be found.');
+                    return null;
+                } else {
+                    return this.add_node(the_parent_node, nodeid, topic, data, idx, direction, expanded);
+                }
+            }
+            var nodeindex = idx || -1;
+            var node = null;
+            if (parent_node.isroot) {
+                var d = jm.direction.right;
+                if (isNaN(direction)) {
+                    var children = parent_node.children;
+                    var children_len = children.length;
+                    var r = 0;
+                    for (var i = 0; i < children_len; i++) { if (children[i].direction === jm.direction.left) { r--; } else { r++; } }
+                    d = (children_len > 1 && r > 0) ? jm.direction.left : jm.direction.right;
+                } else {
+                    d = (direction != jm.direction.left) ? jm.direction.right : jm.direction.left;
+                }
+                node = new jm.node(nodeid, nodeindex, topic, data, false, parent_node, d, expanded);
+            } else {
+                node = new jm.node(nodeid, nodeindex, topic, data, false, parent_node, parent_node.direction, expanded);
+            }
+            if (this._put_node(node)) {
+                parent_node.children.push(node);
+                this._reindex(parent_node);
+            } else {
+                logger.error('fail, the nodeid \'' + node.id + '\' has been already exist.');
+                node = null;
+            }
+            return node;
+        },
+
+        insert_node_before: function (node_before, nodeid, topic, data) {
+            if (!jm.util.is_node(node_before)) {
+                var the_node_before = this.get_node(node_before);
+                if (!the_node_before) {
+                    logger.error('the node_before[id=' + node_before + '] can not be found.');
+                    return null;
+                } else {
+                    return this.insert_node_before(the_node_before, nodeid, topic, data);
+                }
+            }
+            var node_index = node_before.index - 0.5;
+            return this.add_node(node_before.parent, nodeid, topic, data, node_index);
+        },
+
+        get_node_before: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return null;
+                } else {
+                    return this.get_node_before(the_node);
+                }
+            }
+            if (node.isroot) { return null; }
+            var idx = node.index - 2;
+            if (idx >= 0) {
+                return node.parent.children[idx];
+            } else {
+                return null;
+            }
+        },
+
+        insert_node_after: function (node_after, nodeid, topic, data) {
+            if (!jm.util.is_node(node_after)) {
+                var the_node_after = this.get_node(node_after);
+                if (!the_node_after) {
+                    logger.error('the node_after[id=' + node_after + '] can not be found.');
+                    return null;
+                } else {
+                    return this.insert_node_after(the_node_after, nodeid, topic, data);
+                }
+            }
+            var node_index = node_after.index + 0.5;
+            return this.add_node(node_after.parent, nodeid, topic, data, node_index);
+        },
+
+        get_node_after: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return null;
+                } else {
+                    return this.get_node_after(the_node);
+                }
+            }
+            if (node.isroot) { return null; }
+            var idx = node.index;
+            var brothers = node.parent.children;
+            if (brothers.length >= idx) {
+                return node.parent.children[idx];
+            } else {
+                return null;
+            }
+        },
+
+        move_node: function (node, beforeid, parentid, direction) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return null;
+                } else {
+                    return this.move_node(the_node, beforeid, parentid, direction);
+                }
+            }
+            if (!parentid) {
+                parentid = node.parent.id;
+            }
+            return this._move_node(node, beforeid, parentid, direction);
+        },
+
+        _flow_node_direction: function (node, direction) {
+            if (typeof direction === 'undefined') {
+                direction = node.direction;
+            } else {
+                node.direction = direction;
+            }
+            var len = node.children.length;
+            while (len--) {
+                this._flow_node_direction(node.children[len], direction);
+            }
+        },
+
+        _move_node_internal: function (node, beforeid) {
+            if (!!node && !!beforeid) {
+                if (beforeid == '_last_') {
+                    node.index = -1;
+                    this._reindex(node.parent);
+                } else if (beforeid == '_first_') {
+                    node.index = 0;
+                    this._reindex(node.parent);
+                } else {
+                    var node_before = (!!beforeid) ? this.get_node(beforeid) : null;
+                    if (node_before != null && node_before.parent != null && node_before.parent.id == node.parent.id) {
+                        node.index = node_before.index - 0.5;
+                        this._reindex(node.parent);
+                    }
+                }
+            }
+            return node;
+        },
+
+        _move_node: function (node, beforeid, parentid, direction) {
+            if (!!node && !!parentid) {
+                if (node.parent.id != parentid) {
+                    // remove from parent's children
+                    var sibling = node.parent.children;
+                    var si = sibling.length;
+                    while (si--) {
+                        if (sibling[si].id == node.id) {
+                            sibling.splice(si, 1);
+                            break;
+                        }
+                    }
+                    node.parent = this.get_node(parentid);
+                    node.parent.children.push(node);
+                }
+
+                if (node.parent.isroot) {
+                    if (direction == jm.direction.left) {
+                        node.direction = direction;
+                    } else {
+                        node.direction = jm.direction.right;
+                    }
+                } else {
+                    node.direction = node.parent.direction;
+                }
+                this._move_node_internal(node, beforeid);
+                this._flow_node_direction(node);
+            }
+            return node;
+        },
+
+        remove_node: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return false;
+                } else {
+                    return this.remove_node(the_node);
+                }
+            }
+            if (!node) {
+                logger.error('fail, the node can not be found');
+                return false;
+            }
+            if (node.isroot) {
+                logger.error('fail, can not remove root node');
+                return false;
+            }
+            if (this.selected != null && this.selected.id == node.id) {
+                this.selected = null;
+            }
+            // clean all subordinate nodes
+            var children = node.children;
+            var ci = children.length;
+            while (ci--) {
+                this.remove_node(children[ci]);
+            }
+            // clean all children
+            children.length = 0;
+            // remove from parent's children
+            var sibling = node.parent.children;
+            var si = sibling.length;
+            while (si--) {
+                if (sibling[si].id == node.id) {
+                    sibling.splice(si, 1);
+                    break;
+                }
+            }
+            // remove from global nodes
+            delete this.nodes[node.id];
+            // clean all properties
+            for (var k in node) {
+                delete node[k];
+            }
+            // remove it's self
+            node = null;
+            //delete node;
+            return true;
+        },
+
+        _put_node: function (node) {
+            if (node.id in this.nodes) {
+                logger.warn('the nodeid \'' + node.id + '\' has been already exist.');
+                return false;
+            } else {
+                this.nodes[node.id] = node;
+                return true;
+            }
+        },
+
+        _reindex: function (node) {
+            if (node instanceof jm.node) {
+                node.children.sort(jm.node.compare);
+                for (var i = 0; i < node.children.length; i++) {
+                    node.children[i].index = i + 1;
+                }
+            }
+        },
+    };
+
+    jm.format = {
+        node_tree: {
+            example: {
+                "meta": {
+                    "name": __name__,
+                    "author": __author__,
+                    "version": __version__
+                },
+                "format": "node_tree",
+                "data": { "id": "root", "topic": "jsMind Example" }
+            },
+            get_mind: function (source) {
+                var df = jm.format.node_tree;
+                var mind = new jm.mind();
+                mind.name = source.meta.name;
+                mind.author = source.meta.author;
+                mind.version = source.meta.version;
+                df._parse(mind, source.data);
+                return mind;
+            },
+            get_data: function (mind) {
+                var df = jm.format.node_tree;
+                var json = {};
+                json.meta = {
+                    name: mind.name,
+                    author: mind.author,
+                    version: mind.version
+                };
+                json.format = 'node_tree';
+                json.data = df._buildnode(mind.root);
+                return json;
+            },
+
+            _parse: function (mind, node_root) {
+                var df = jm.format.node_tree;
+                var data = df._extract_data(node_root);
+                mind.set_root(node_root.id, node_root.topic, data);
+                if ('children' in node_root) {
+                    var children = node_root.children;
+                    for (var i = 0; i < children.length; i++) {
+                        df._extract_subnode(mind, mind.root, children[i]);
+                    }
+                }
+            },
+
+            _extract_data: function (node_json) {
+                var data = {};
+                for (var k in node_json) {
+                    if (k == 'id' || k == 'topic' || k == 'children' || k == 'direction' || k == 'expanded') {
+                        continue;
+                    }
+                    data[k] = node_json[k];
+                }
+                return data;
+            },
+
+            _extract_subnode: function (mind, node_parent, node_json) {
+                var df = jm.format.node_tree;
+                var data = df._extract_data(node_json);
+                var d = null;
+                if (node_parent.isroot) {
+                    d = node_json.direction == 'left' ? jm.direction.left : jm.direction.right;
+                }
+                var node = mind.add_node(node_parent, node_json.id, node_json.topic, data, null, d, node_json.expanded);
+                if (!!node_json['children']) {
+                    var children = node_json.children;
+                    for (var i = 0; i < children.length; i++) {
+                        df._extract_subnode(mind, node, children[i]);
+                    }
+                }
+            },
+
+            _buildnode: function (node) {
+                var df = jm.format.node_tree;
+                if (!(node instanceof jm.node)) { return; }
+                var o = {
+                    id: node.id,
+                    topic: node.topic,
+                    expanded: node.expanded
+                };
+                if (!!node.parent && node.parent.isroot) {
+                    o.direction = node.direction == jm.direction.left ? 'left' : 'right';
+                }
+                if (node.data != null) {
+                    var node_data = node.data;
+                    for (var k in node_data) {
+                        o[k] = node_data[k];
+                    }
+                }
+                var children = node.children;
+                if (children.length > 0) {
+                    o.children = [];
+                    for (var i = 0; i < children.length; i++) {
+                        o.children.push(df._buildnode(children[i]));
+                    }
+                }
+                return o;
+            }
+        },
+
+        node_array: {
+            example: {
+                "meta": {
+                    "name": __name__,
+                    "author": __author__,
+                    "version": __version__
+                },
+                "format": "node_array",
+                "data": [
+                    { "id": "root", "topic": "jsMind Example", "isroot": true }
+                ]
+            },
+
+            get_mind: function (source) {
+                var df = jm.format.node_array;
+                var mind = new jm.mind();
+                mind.name = source.meta.name;
+                mind.author = source.meta.author;
+                mind.version = source.meta.version;
+                df._parse(mind, source.data);
+                return mind;
+            },
+
+            get_data: function (mind) {
+                var df = jm.format.node_array;
+                var json = {};
+                json.meta = {
+                    name: mind.name,
+                    author: mind.author,
+                    version: mind.version
+                };
+                json.format = 'node_array';
+                json.data = [];
+                df._array(mind, json.data);
+                return json;
+            },
+
+            _parse: function (mind, node_array) {
+                var df = jm.format.node_array;
+                var narray = node_array.slice(0);
+                // reverse array for improving looping performance
+                narray.reverse();
+                var root_id = df._extract_root(mind, narray);
+                if (!!root_id) {
+                    df._extract_subnode(mind, root_id, narray);
+                } else {
+                    logger.error('root node can not be found');
+                }
+            },
+
+            _extract_root: function (mind, node_array) {
+                var df = jm.format.node_array;
+                var i = node_array.length;
+                while (i--) {
+                    if ('isroot' in node_array[i] && node_array[i].isroot) {
+                        var root_json = node_array[i];
+                        var data = df._extract_data(root_json);
+                        mind.set_root(root_json.id, root_json.topic, data);
+                        node_array.splice(i, 1);
+                        return root_json.id;
+                    }
+                }
+                return null;
+            },
+
+            _extract_subnode: function (mind, parentid, node_array) {
+                var df = jm.format.node_array;
+                var i = node_array.length;
+                var node_json = null;
+                var data = null;
+                var extract_count = 0;
+                while (i--) {
+                    node_json = node_array[i];
+                    if (node_json.parentid == parentid) {
+                        data = df._extract_data(node_json);
+                        var d = null;
+                        var node_direction = node_json.direction;
+                        if (!!node_direction) {
+                            d = node_direction == 'left' ? jm.direction.left : jm.direction.right;
+                        }
+                        mind.add_node(parentid, node_json.id, node_json.topic, data, null, d, node_json.expanded);
+                        node_array.splice(i, 1);
+                        extract_count++;
+                        var sub_extract_count = df._extract_subnode(mind, node_json.id, node_array);
+                        if (sub_extract_count > 0) {
+                            // reset loop index after extract subordinate node
+                            i = node_array.length;
+                            extract_count += sub_extract_count;
+                        }
+                    }
+                }
+                return extract_count;
+            },
+
+            _extract_data: function (node_json) {
+                var data = {};
+                for (var k in node_json) {
+                    if (k == 'id' || k == 'topic' || k == 'parentid' || k == 'isroot' || k == 'direction' || k == 'expanded') {
+                        continue;
+                    }
+                    data[k] = node_json[k];
+                }
+                return data;
+            },
+
+            _array: function (mind, node_array) {
+                var df = jm.format.node_array;
+                df._array_node(mind.root, node_array);
+            },
+
+            _array_node: function (node, node_array) {
+                var df = jm.format.node_array;
+                if (!(node instanceof jm.node)) { return; }
+                var o = {
+                    id: node.id,
+                    topic: node.topic,
+                    expanded: node.expanded
+                };
+                if (!!node.parent) {
+                    o.parentid = node.parent.id;
+                }
+                if (node.isroot) {
+                    o.isroot = true;
+                }
+                if (!!node.parent && node.parent.isroot) {
+                    o.direction = node.direction == jm.direction.left ? 'left' : 'right';
+                }
+                if (node.data != null) {
+                    var node_data = node.data;
+                    for (var k in node_data) {
+                        o[k] = node_data[k];
+                    }
+                }
+                node_array.push(o);
+                var ci = node.children.length;
+                for (var i = 0; i < ci; i++) {
+                    df._array_node(node.children[i], node_array);
+                }
+            },
+        },
+
+        freemind: {
+            example: {
+                "meta": {
+                    "name": __name__,
+                    "author": __author__,
+                    "version": __version__
+                },
+                "format": "freemind",
+                "data": "<map version=\"1.0.1\"><node ID=\"root\" TEXT=\"freemind Example\"/></map>"
+            },
+            get_mind: function (source) {
+                var df = jm.format.freemind;
+                var mind = new jm.mind();
+                mind.name = source.meta.name;
+                mind.author = source.meta.author;
+                mind.version = source.meta.version;
+                var xml = source.data;
+                var xml_doc = df._parse_xml(xml);
+                var xml_root = df._find_root(xml_doc);
+                df._load_node(mind, null, xml_root);
+                return mind;
+            },
+
+            get_data: function (mind) {
+                var df = jm.format.freemind;
+                var json = {};
+                json.meta = {
+                    name: mind.name,
+                    author: mind.author,
+                    version: mind.version
+                };
+                json.format = 'freemind';
+                var xmllines = [];
+                xmllines.push('<map version=\"1.0.1\">');
+                df._buildmap(mind.root, xmllines);
+                xmllines.push('</map>');
+                json.data = xmllines.join(' ');
+                return json;
+            },
+
+            _parse_xml: function (xml) {
+                var xml_doc = null;
+                if (window.DOMParser) {
+                    var parser = new DOMParser();
+                    xml_doc = parser.parseFromString(xml, 'text/xml');
+                } else { // Internet Explorer
+                    xml_doc = new ActiveXObject('Microsoft.XMLDOM');
+                    xml_doc.async = false;
+                    xml_doc.loadXML(xml);
+                }
+                return xml_doc;
+            },
+
+            _find_root: function (xml_doc) {
+                var nodes = xml_doc.childNodes;
+                var node = null;
+                var root = null;
+                var n = null;
+                for (var i = 0; i < nodes.length; i++) {
+                    n = nodes[i];
+                    if (n.nodeType == 1 && n.tagName == 'map') {
+                        node = n;
+                        break;
+                    }
+                }
+                if (!!node) {
+                    var ns = node.childNodes;
+                    node = null;
+                    for (var i = 0; i < ns.length; i++) {
+                        n = ns[i];
+                        if (n.nodeType == 1 && n.tagName == 'node') {
+                            node = n;
+                            break;
+                        }
+                    }
+                }
+                return node;
+            },
+
+            _load_node: function (mind, parent_id, xml_node) {
+                var df = jm.format.freemind;
+                var node_id = xml_node.getAttribute('ID');
+                var node_topic = xml_node.getAttribute('TEXT');
+                // look for richcontent
+                if (node_topic == null) {
+                    var topic_children = xml_node.childNodes;
+                    var topic_child = null;
+                    for (var i = 0; i < topic_children.length; i++) {
+                        topic_child = topic_children[i];
+                        //logger.debug(topic_child.tagName);
+                        if (topic_child.nodeType == 1 && topic_child.tagName === 'richcontent') {
+                            node_topic = topic_child.textContent;
+                            break;
+                        }
+                    }
+                }
+                var node_data = df._load_attributes(xml_node);
+                var node_expanded = ('expanded' in node_data) ? (node_data.expanded == 'true') : true;
+                delete node_data.expanded;
+
+                var node_position = xml_node.getAttribute('POSITION');
+                var node_direction = null;
+                if (!!node_position) {
+                    node_direction = node_position == 'left' ? jm.direction.left : jm.direction.right;
+                }
+                //logger.debug(node_position +':'+ node_direction);
+                if (!!parent_id) {
+                    mind.add_node(parent_id, node_id, node_topic, node_data, null, node_direction, node_expanded);
+                } else {
+                    mind.set_root(node_id, node_topic, node_data);
+                }
+                var children = xml_node.childNodes;
+                var child = null;
+                for (var i = 0; i < children.length; i++) {
+                    child = children[i];
+                    if (child.nodeType == 1 && child.tagName == 'node') {
+                        df._load_node(mind, node_id, child);
+                    }
+                }
+            },
+
+            _load_attributes: function (xml_node) {
+                var children = xml_node.childNodes;
+                var attr = null;
+                var attr_data = {};
+                for (var i = 0; i < children.length; i++) {
+                    attr = children[i];
+                    if (attr.nodeType == 1 && attr.tagName === 'attribute') {
+                        attr_data[attr.getAttribute('NAME')] = attr.getAttribute('VALUE');
+                    }
+                }
+                return attr_data;
+            },
+
+            _buildmap: function (node, xmllines) {
+                var df = jm.format.freemind;
+                var pos = null;
+                if (!!node.parent && node.parent.isroot) {
+                    pos = node.direction === jm.direction.left ? 'left' : 'right';
+                }
+                xmllines.push('<node');
+                xmllines.push('ID=\"' + node.id + '\"');
+                if (!!pos) {
+                    xmllines.push('POSITION=\"' + pos + '\"');
+                }
+                xmllines.push('TEXT=\"' + node.topic + '\">');
+
+                // store expanded status as an attribute
+                xmllines.push('<attribute NAME=\"expanded\" VALUE=\"' + node.expanded + '\"/>');
+
+                // for attributes
+                var node_data = node.data;
+                if (node_data != null) {
+                    for (var k in node_data) {
+                        xmllines.push('<attribute NAME=\"' + k + '\" VALUE=\"' + node_data[k] + '\"/>');
+                    }
+                }
+
+                // for children
+                var children = node.children;
+                for (var i = 0; i < children.length; i++) {
+                    df._buildmap(children[i], xmllines);
+                }
+
+                xmllines.push('</node>');
+            },
+        },
+    };
+
+    // ============= utility object =============================================
+
+    jm.util = {
+        is_node: function (node) {
+            return !!node && node instanceof jm.node;
+        },
+        ajax: {
+            _xhr: function () {
+                var xhr = null;
+                if (window.XMLHttpRequest) {
+                    xhr = new XMLHttpRequest();
+                } else {
+                    try {
+                        xhr = new ActiveXObject('Microsoft.XMLHTTP');
+                    } catch (e) { }
+                }
+                return xhr;
+            },
+            _eurl: function (url) {
+                return encodeURIComponent(url);
+            },
+            request: function (url, param, method, callback, fail_callback) {
+                var a = jm.util.ajax;
+                var p = null;
+                var tmp_param = [];
+                for (var k in param) {
+                    tmp_param.push(a._eurl(k) + '=' + a._eurl(param[k]));
+                }
+                if (tmp_param.length > 0) {
+                    p = tmp_param.join('&');
+                }
+                var xhr = a._xhr();
+                if (!xhr) { return; }
+                xhr.onreadystatechange = function () {
+                    if (xhr.readyState == 4) {
+                        if (xhr.status == 200 || xhr.status == 0) {
+                            if (typeof callback === 'function') {
+                                var data = jm.util.json.string2json(xhr.responseText);
+                                if (data != null) {
+                                    callback(data);
+                                } else {
+                                    callback(xhr.responseText);
+                                }
+                            }
+                        } else {
+                            if (typeof fail_callback === 'function') {
+                                fail_callback(xhr);
+                            } else {
+                                logger.error('xhr request failed.', xhr);
+                            }
+                        }
+                    }
+                }
+                method = method || 'GET';
+                xhr.open(method, url, true);
+                xhr.setRequestHeader('If-Modified-Since', '0');
+                if (method == 'POST') {
+                    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
+                    xhr.send(p);
+                } else {
+                    xhr.send();
+                }
+            },
+            get: function (url, callback) {
+                return jm.util.ajax.request(url, {}, 'GET', callback);
+            },
+            post: function (url, param, callback) {
+                return jm.util.ajax.request(url, param, 'POST', callback);
+            }
+        },
+
+        dom: {
+            //target,eventType,handler
+            add_event: function (t, e, h) {
+                if (!!t.addEventListener) {
+                    t.addEventListener(e, h, false);
+                } else {
+                    t.attachEvent('on' + e, h);
+                }
+            }
+        },
+
+        file: {
+            read: function (file_data, fn_callback) {
+                var reader = new FileReader();
+                reader.onload = function () {
+                    if (typeof fn_callback === 'function') {
+                        fn_callback(this.result, file_data.name);
+                    }
+                };
+                reader.readAsText(file_data);
+            },
+
+            save: function (file_data, type, name) {
+                var blob;
+                if (typeof $w.Blob === 'function') {
+                    blob = new Blob([file_data], { type: type });
+                } else {
+                    var BlobBuilder = $w.BlobBuilder || $w.MozBlobBuilder || $w.WebKitBlobBuilder || $w.MSBlobBuilder;
+                    var bb = new BlobBuilder();
+                    bb.append(file_data);
+                    blob = bb.getBlob(type);
+                }
+                if (navigator.msSaveBlob) {
+                    navigator.msSaveBlob(blob, name);
+                } else {
+                    var URL = $w.URL || $w.webkitURL;
+                    var bloburl = URL.createObjectURL(blob);
+                    var anchor = $c('a');
+                    if ('download' in anchor) {
+                        anchor.style.visibility = 'hidden';
+                        anchor.href = bloburl;
+                        anchor.download = name;
+                        $d.body.appendChild(anchor);
+                        var evt = $d.createEvent('MouseEvents');
+                        evt.initEvent('click', true, true);
+                        anchor.dispatchEvent(evt);
+                        $d.body.removeChild(anchor);
+                    } else {
+                        location.href = bloburl;
+                    }
+                }
+            }
+        },
+
+        json: {
+            json2string: function (json) {
+                if (!!JSON) {
+                    try {
+                        var json_str = JSON.stringify(json);
+                        return json_str;
+                    } catch (e) {
+                        logger.warn(e);
+                        logger.warn('can not convert to string');
+                        return null;
+                    }
+                }
+            },
+            string2json: function (json_str) {
+                if (!!JSON) {
+                    try {
+                        var json = JSON.parse(json_str);
+                        return json;
+                    } catch (e) {
+                        logger.warn(e);
+                        logger.warn('can not parse to json');
+                        return null;
+                    }
+                }
+            },
+            merge: function (b, a) {
+                for (var o in a) {
+                    if (o in b) {
+                        if (typeof b[o] === 'object' &&
+                            Object.prototype.toString.call(b[o]).toLowerCase() == '[object object]' &&
+                            !b[o].length) {
+                            jm.util.json.merge(b[o], a[o]);
+                        } else {
+                            b[o] = a[o];
+                        }
+                    } else {
+                        b[o] = a[o];
+                    }
+                }
+                return b;
+            }
+        },
+
+        uuid: {
+            newid: function () {
+                return (new Date().getTime().toString(16) + Math.random().toString(16).substr(2)).substr(2, 16);
+            }
+        },
+
+        text: {
+            is_empty: function (s) {
+                if (!s) { return true; }
+                return s.replace(/\s*/, '').length == 0;
+            }
+        }
+    };
+
+    jm.prototype = {
+        init: function () {
+            if (this.inited) { return; }
+            this.inited = true;
+
+            var opts = this.options;
+
+            var opts_layout = {
+                mode: opts.mode,
+                hspace: opts.layout.hspace,
+                vspace: opts.layout.vspace,
+                pspace: opts.layout.pspace
+            }
+            var opts_view = {
+                container: opts.container,
+                support_html: opts.support_html,
+                engine: opts.view.engine,
+                hmargin: opts.view.hmargin,
+                vmargin: opts.view.vmargin,
+                line_width: opts.view.line_width,
+                line_color: opts.view.line_color
+            };
+            // create instance of function provider
+            this.data = new jm.data_provider(this);
+            this.layout = new jm.layout_provider(this, opts_layout);
+            this.view = new jm.view_provider(this, opts_view);
+            this.shortcut = new jm.shortcut_provider(this, opts.shortcut);
+
+            this.data.init();
+            this.layout.init();
+            this.view.init();
+            this.shortcut.init();
+
+            this._event_bind();
+
+            jm.init_plugins(this);
+        },
+
+        enable_edit: function () {
+            this.options.editable = true;
+        },
+
+        disable_edit: function () {
+            this.options.editable = false;
+        },
+
+        // call enable_event_handle('dblclick')
+        // options are 'mousedown', 'click', 'dblclick'
+        enable_event_handle: function (event_handle) {
+            this.options.default_event_handle['enable_' + event_handle + '_handle'] = true;
+        },
+
+        // call disable_event_handle('dblclick')
+        // options are 'mousedown', 'click', 'dblclick'
+        disable_event_handle: function (event_handle) {
+            this.options.default_event_handle['enable_' + event_handle + '_handle'] = false;
+        },
+
+        get_editable: function () {
+            return this.options.editable;
+        },
+
+        set_theme: function (theme) {
+            var theme_old = this.options.theme;
+            this.options.theme = (!!theme) ? theme : null;
+            if (theme_old != this.options.theme) {
+                this.view.reset_theme();
+                this.view.reset_custom_style();
+            }
+        },
+        _event_bind: function () {
+            this.view.add_event(this, 'mousedown', this.mousedown_handle);
+            this.view.add_event(this, 'click', this.click_handle);
+            this.view.add_event(this, 'dblclick', this.dblclick_handle);
+        },
+
+        mousedown_handle: function (e) {
+            if (!this.options.default_event_handle['enable_mousedown_handle']) {
+                return;
+            }
+            var element = e.target || event.srcElement;
+            var nodeid = this.view.get_binded_nodeid(element);
+            if (!!nodeid) {
+                if (element.tagName.toLowerCase() == 'jmnode') {
+                    this.select_node(nodeid);
+                }
+            } else {
+                this.select_clear();
+            }
+        },
+
+        click_handle: function (e) {
+            if (!this.options.default_event_handle['enable_click_handle']) {
+                return;
+            }
+            var element = e.target || event.srcElement;
+            var isexpander = this.view.is_expander(element);
+            if (isexpander) {
+                var nodeid = this.view.get_binded_nodeid(element);
+                if (!!nodeid) {
+                    this.toggle_node(nodeid);
+                }
+            }
+        },
+
+        dblclick_handle: function (e) {
+            if (!this.options.default_event_handle['enable_dblclick_handle']) {
+                return;
+            }
+            if (this.get_editable()) {
+                var element = e.target || event.srcElement;
+                var nodeid = this.view.get_binded_nodeid(element);
+                if (!!nodeid) {
+                    this.begin_edit(nodeid);
+                }
+            }
+        },
+
+        begin_edit: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return false;
+                } else {
+                    return this.begin_edit(the_node);
+                }
+            }
+            if (this.get_editable()) {
+                this.view.edit_node_begin(node);
+            } else {
+                logger.error('fail, this mind map is not editable.');
+                return;
+            }
+        },
+
+        end_edit: function () {
+            this.view.edit_node_end();
+        },
+
+        toggle_node: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return;
+                } else {
+                    return this.toggle_node(the_node);
+                }
+            }
+            if (node.isroot) { return; }
+            this.view.save_location(node);
+            this.layout.toggle_node(node);
+            this.view.relayout();
+            this.view.restore_location(node);
+        },
+
+        expand_node: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return;
+                } else {
+                    return this.expand_node(the_node);
+                }
+            }
+            if (node.isroot) { return; }
+            this.view.save_location(node);
+            this.layout.expand_node(node);
+            this.view.relayout();
+            this.view.restore_location(node);
+        },
+
+        collapse_node: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return;
+                } else {
+                    return this.collapse_node(the_node);
+                }
+            }
+            if (node.isroot) { return; }
+            this.view.save_location(node);
+            this.layout.collapse_node(node);
+            this.view.relayout();
+            this.view.restore_location(node);
+        },
+
+        expand_all: function () {
+            this.layout.expand_all();
+            this.view.relayout();
+        },
+
+        collapse_all: function () {
+            this.layout.collapse_all();
+            this.view.relayout();
+        },
+
+        expand_to_depth: function (depth) {
+            this.layout.expand_to_depth(depth);
+            this.view.relayout();
+        },
+
+        _reset: function () {
+            this.view.reset();
+            this.layout.reset();
+            this.data.reset();
+        },
+
+        _show: function (mind) {
+            var m = mind || jm.format.node_array.example;
+
+            this.mind = this.data.load(m);
+            if (!this.mind) {
+                logger.error('data.load error');
+                return;
+            } else {
+                logger.debug('data.load ok');
+            }
+
+            this.view.load();
+            logger.debug('view.load ok');
+
+            this.layout.layout();
+            logger.debug('layout.layout ok');
+
+            this.view.show(true);
+            logger.debug('view.show ok');
+
+            this.invoke_event_handle(jm.event_type.show, { data: [mind] });
+        },
+
+        show: function (mind) {
+            this._reset();
+            this._show(mind);
+        },
+
+        get_meta: function () {
+            return {
+                name: this.mind.name,
+                author: this.mind.author,
+                version: this.mind.version
+            };
+        },
+
+        get_data: function (data_format) {
+            var df = data_format || 'node_tree';
+            return this.data.get_data(df);
+        },
+
+        get_root: function () {
+            return this.mind.root;
+        },
+
+        get_node: function (nodeid) {
+            return this.mind.get_node(nodeid);
+        },
+
+        add_node: function (parent_node, nodeid, topic, data) {
+            if (this.get_editable()) {
+                var node = this.mind.add_node(parent_node, nodeid, topic, data);
+                if (!!node) {
+                    this.view.add_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                    this.view.reset_node_custom_style(node);
+                    this.expand_node(parent_node);
+                    this.invoke_event_handle(jm.event_type.edit, { evt: 'add_node', data: [parent_node.id, nodeid, topic, data], node: nodeid });
+                }
+                return node;
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        insert_node_before: function (node_before, nodeid, topic, data) {
+            if (this.get_editable()) {
+                var beforeid = jm.util.is_node(node_before) ? node_before.id : node_before;
+                var node = this.mind.insert_node_before(node_before, nodeid, topic, data);
+                if (!!node) {
+                    this.view.add_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                    this.invoke_event_handle(jm.event_type.edit, { evt: 'insert_node_before', data: [beforeid, nodeid, topic, data], node: nodeid });
+                }
+                return node;
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        insert_node_after: function (node_after, nodeid, topic, data) {
+            if (this.get_editable()) {
+                var afterid = jm.util.is_node(node_after) ? node_after.id : node_after;
+                var node = this.mind.insert_node_after(node_after, nodeid, topic, data);
+                if (!!node) {
+                    this.view.add_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                    this.invoke_event_handle(jm.event_type.edit, { evt: 'insert_node_after', data: [afterid, nodeid, topic, data], node: nodeid });
+                }
+                return node;
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        remove_node: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return false;
+                } else {
+                    return this.remove_node(the_node);
+                }
+            }
+            if (this.get_editable()) {
+                if (node.isroot) {
+                    logger.error('fail, can not remove root node');
+                    return false;
+                }
+                var nodeid = node.id;
+                var parentid = node.parent.id;
+                var parent_node = this.get_node(parentid);
+                this.view.save_location(parent_node);
+                this.view.remove_node(node);
+                this.mind.remove_node(node);
+                this.layout.layout();
+                this.view.show(false);
+                this.view.restore_location(parent_node);
+                this.invoke_event_handle(jm.event_type.edit, { evt: 'remove_node', data: [nodeid], node: parentid });
+                return true;
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return false;
+            }
+        },
+
+        update_node: function (nodeid, topic) {
+            if (this.get_editable()) {
+                if (jm.util.text.is_empty(topic)) {
+                    logger.warn('fail, topic can not be empty');
+                    return;
+                }
+                var node = this.get_node(nodeid);
+                if (!!node) {
+                    if (node.topic === topic) {
+                        logger.info('nothing changed');
+                        this.view.update_node(node);
+                        return;
+                    }
+                    node.topic = topic;
+                    this.view.update_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                    this.invoke_event_handle(jm.event_type.edit, { evt: 'update_node', data: [nodeid, topic], node: nodeid });
+                }
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return;
+            }
+        },
+
+        move_node: function (nodeid, beforeid, parentid, direction) {
+            if (this.get_editable()) {
+                var node = this.mind.move_node(nodeid, beforeid, parentid, direction);
+                if (!!node) {
+                    this.view.update_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                    this.invoke_event_handle(jm.event_type.edit, { evt: 'move_node', data: [nodeid, beforeid, parentid, direction], node: nodeid });
+                }
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return;
+            }
+        },
+
+        select_node: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return;
+                } else {
+                    return this.select_node(the_node);
+                }
+            }
+            if (!this.layout.is_visible(node)) {
+                return;
+            }
+            this.mind.selected = node;
+            this.view.select_node(node);
+            this.invoke_event_handle(jm.event_type.select, { evt: 'select_node', data: [], node: node.id });
+        },
+
+        get_selected_node: function () {
+            if (!!this.mind) {
+                return this.mind.selected;
+            } else {
+                return null;
+            }
+        },
+
+        select_clear: function () {
+            if (!!this.mind) {
+                this.mind.selected = null;
+                this.view.select_clear();
+            }
+        },
+
+        is_node_visible: function (node) {
+            return this.layout.is_visible(node);
+        },
+
+        find_node_before: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return;
+                } else {
+                    return this.find_node_before(the_node);
+                }
+            }
+            if (node.isroot) { return null; }
+            var n = null;
+            if (node.parent.isroot) {
+                var c = node.parent.children;
+                var prev = null;
+                var ni = null;
+                for (var i = 0; i < c.length; i++) {
+                    ni = c[i];
+                    if (node.direction === ni.direction) {
+                        if (node.id === ni.id) {
+                            n = prev;
+                        }
+                        prev = ni;
+                    }
+                }
+            } else {
+                n = this.mind.get_node_before(node);
+            }
+            return n;
+        },
+
+        find_node_after: function (node) {
+            if (!jm.util.is_node(node)) {
+                var the_node = this.get_node(node);
+                if (!the_node) {
+                    logger.error('the node[id=' + node + '] can not be found.');
+                    return;
+                } else {
+                    return this.find_node_after(the_node);
+                }
+            }
+            if (node.isroot) { return null; }
+            var n = null;
+            if (node.parent.isroot) {
+                var c = node.parent.children;
+                var getthis = false;
+                var ni = null;
+                for (var i = 0; i < c.length; i++) {
+                    ni = c[i];
+                    if (node.direction === ni.direction) {
+                        if (getthis) {
+                            n = ni;
+                            break;
+                        }
+                        if (node.id === ni.id) {
+                            getthis = true;
+                        }
+                    }
+                }
+            } else {
+                n = this.mind.get_node_after(node);
+            }
+            return n;
+        },
+
+        set_node_color: function (nodeid, bgcolor, fgcolor) {
+            if (this.get_editable()) {
+                var node = this.mind.get_node(nodeid);
+                if (!!node) {
+                    if (!!bgcolor) {
+                        node.data['background-color'] = bgcolor;
+                    }
+                    if (!!fgcolor) {
+                        node.data['foreground-color'] = fgcolor;
+                    }
+                    this.view.reset_node_custom_style(node);
+                }
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        set_node_font_style: function (nodeid, size, weight, style) {
+            if (this.get_editable()) {
+                var node = this.mind.get_node(nodeid);
+                if (!!node) {
+                    if (!!size) {
+                        node.data['font-size'] = size;
+                    }
+                    if (!!weight) {
+                        node.data['font-weight'] = weight;
+                    }
+                    if (!!style) {
+                        node.data['font-style'] = style;
+                    }
+                    this.view.reset_node_custom_style(node);
+                    this.view.update_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                }
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        set_node_background_image: function (nodeid, image, width, height, rotation) {
+            if (this.get_editable()) {
+                var node = this.mind.get_node(nodeid);
+                if (!!node) {
+                    if (!!image) {
+                        node.data['background-image'] = image;
+                    }
+                    if (!!width) {
+                        node.data['width'] = width;
+                    }
+                    if (!!height) {
+                        node.data['height'] = height;
+                    }
+                    if (!!rotation) {
+                        node.data['background-rotation'] = rotation;
+                    }
+                    this.view.reset_node_custom_style(node);
+                    this.view.update_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                }
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        set_node_background_rotation: function (nodeid, rotation) {
+            if (this.get_editable()) {
+                var node = this.mind.get_node(nodeid);
+                if (!!node) {
+                    if (!node.data['background-image']) {
+                        logger.error('fail, only can change rotation angle of node with background image');
+                        return null;
+                    }
+                    node.data['background-rotation'] = rotation;
+                    this.view.reset_node_custom_style(node);
+                    this.view.update_node(node);
+                    this.layout.layout();
+                    this.view.show(false);
+                }
+            } else {
+                logger.error('fail, this mind map is not editable');
+                return null;
+            }
+        },
+
+        resize: function () {
+            this.view.resize();
+        },
+
+        // callback(type ,data)
+        add_event_listener: function (callback) {
+            if (typeof callback === 'function') {
+                this.event_handles.push(callback);
+            }
+        },
+
+        clear_event_listener: function () {
+            this.event_handles = [];
+        },
+
+        invoke_event_handle: function (type, data) {
+            var j = this;
+            $w.setTimeout(function () {
+                j._invoke_event_handle(type, data);
+            }, 0);
+        },
+
+        _invoke_event_handle: function (type, data) {
+            var l = this.event_handles.length;
+            for (var i = 0; i < l; i++) {
+                this.event_handles[i](type, data);
+            }
+        }
+
+    };
+
+    // ============= data provider =============================================
+
+    jm.data_provider = function (jm) {
+        this.jm = jm;
+    };
+
+    jm.data_provider.prototype = {
+        init: function () {
+            logger.debug('data.init');
+        },
+
+        reset: function () {
+            logger.debug('data.reset');
+        },
+
+        load: function (mind_data) {
+            var df = null;
+            var mind = null;
+            if (typeof mind_data === 'object') {
+                if (!!mind_data.format) {
+                    df = mind_data.format;
+                } else {
+                    df = 'node_tree';
+                }
+            } else {
+                df = 'freemind';
+            }
+
+            if (df == 'node_array') {
+                mind = jm.format.node_array.get_mind(mind_data);
+            } else if (df == 'node_tree') {
+                mind = jm.format.node_tree.get_mind(mind_data);
+            } else if (df == 'freemind') {
+                mind = jm.format.freemind.get_mind(mind_data);
+            } else {
+                logger.warn('unsupported format');
+            }
+            return mind;
+        },
+
+        get_data: function (data_format) {
+            var data = null;
+            if (data_format == 'node_array') {
+                data = jm.format.node_array.get_data(this.jm.mind);
+            } else if (data_format == 'node_tree') {
+                data = jm.format.node_tree.get_data(this.jm.mind);
+            } else if (data_format == 'freemind') {
+                data = jm.format.freemind.get_data(this.jm.mind);
+            } else {
+                logger.error('unsupported ' + data_format + ' format');
+            }
+            return data;
+        },
+    };
+
+    // ============= layout provider ===========================================
+
+    jm.layout_provider = function (jm, options) {
+        this.opts = options;
+        this.jm = jm;
+        this.isside = (this.opts.mode == 'side');
+        this.bounds = null;
+
+        this.cache_valid = false;
+    };
+
+    jm.layout_provider.prototype = {
+        init: function () {
+            logger.debug('layout.init');
+        },
+        reset: function () {
+            logger.debug('layout.reset');
+            this.bounds = { n: 0, s: 0, w: 0, e: 0 };
+        },
+        layout: function () {
+            logger.debug('layout.layout');
+            this.layout_direction();
+            this.layout_offset();
+        },
+
+        layout_direction: function () {
+            this._layout_direction_root();
+        },
+
+        _layout_direction_root: function () {
+            var node = this.jm.mind.root;
+            // logger.debug(node);
+            var layout_data = null;
+            if ('layout' in node._data) {
+                layout_data = node._data.layout;
+            } else {
+                layout_data = {};
+                node._data.layout = layout_data;
+            }
+            var children = node.children;
+            var children_count = children.length;
+            layout_data.direction = jm.direction.center;
+            layout_data.side_index = 0;
+            if (this.isside) {
+                var i = children_count;
+                while (i--) {
+                    this._layout_direction_side(children[i], jm.direction.right, i);
+                }
+            } else {
+                var i = children_count;
+                var subnode = null;
+                while (i--) {
+                    subnode = children[i];
+                    if (subnode.direction == jm.direction.left) {
+                        this._layout_direction_side(subnode, jm.direction.left, i);
+                    } else {
+                        this._layout_direction_side(subnode, jm.direction.right, i);
+                    }
+                }
+                /*
+                var boundary = Math.ceil(children_count/2);
+                var i = children_count;
+                while(i--){
+                    if(i>=boundary){
+                        this._layout_direction_side(children[i],jm.direction.left, children_count-i-1);
+                    }else{
+                        this._layout_direction_side(children[i],jm.direction.right, i);
+                    }
+                }*/
+
+            }
+        },
+
+        _layout_direction_side: function (node, direction, side_index) {
+            var layout_data = null;
+            if ('layout' in node._data) {
+                layout_data = node._data.layout;
+            } else {
+                layout_data = {};
+                node._data.layout = layout_data;
+            }
+            var children = node.children;
+            var children_count = children.length;
+
+            layout_data.direction = direction;
+            layout_data.side_index = side_index;
+            var i = children_count;
+            while (i--) {
+                this._layout_direction_side(children[i], direction, i);
+            }
+        },
+
+        layout_offset: function () {
+            var node = this.jm.mind.root;
+            var layout_data = node._data.layout;
+            layout_data.offset_x = 0;
+            layout_data.offset_y = 0;
+            layout_data.outer_height = 0;
+            var children = node.children;
+            var i = children.length;
+            var left_nodes = [];
+            var right_nodes = [];
+            var subnode = null;
+            while (i--) {
+                subnode = children[i];
+                if (subnode._data.layout.direction == jm.direction.right) {
+                    right_nodes.unshift(subnode);
+                } else {
+                    left_nodes.unshift(subnode);
+                }
+            }
+            layout_data.left_nodes = left_nodes;
+            layout_data.right_nodes = right_nodes;
+            layout_data.outer_height_left = this._layout_offset_subnodes(left_nodes);
+            layout_data.outer_height_right = this._layout_offset_subnodes(right_nodes);
+            this.bounds.e = node._data.view.width / 2;
+            this.bounds.w = 0 - this.bounds.e;
+            //logger.debug(this.bounds.w);
+            this.bounds.n = 0;
+            this.bounds.s = Math.max(layout_data.outer_height_left, layout_data.outer_height_right);
+        },
+
+        // layout both the x and y axis
+        _layout_offset_subnodes: function (nodes) {
+            var total_height = 0;
+            var nodes_count = nodes.length;
+            var i = nodes_count;
+            var node = null;
+            var node_outer_height = 0;
+            var layout_data = null;
+            var base_y = 0;
+            var pd = null; // parent._data
+            while (i--) {
+                node = nodes[i];
+                layout_data = node._data.layout;
+                if (pd == null) {
+                    pd = node.parent._data;
+                }
+
+                node_outer_height = this._layout_offset_subnodes(node.children);
+                if (!node.expanded) {
+                    node_outer_height = 0;
+                    this.set_visible(node.children, false);
+                }
+                node_outer_height = Math.max(node._data.view.height, node_outer_height);
+
+                layout_data.outer_height = node_outer_height;
+                layout_data.offset_y = base_y - node_outer_height / 2;
+                layout_data.offset_x = this.opts.hspace * layout_data.direction + pd.view.width * (pd.layout.direction + layout_data.direction) / 2;
+                if (!node.parent.isroot) {
+                    layout_data.offset_x += this.opts.pspace * layout_data.direction;
+                }
+
+                base_y = base_y - node_outer_height - this.opts.vspace;
+                total_height += node_outer_height;
+            }
+            if (nodes_count > 1) {
+                total_height += this.opts.vspace * (nodes_count - 1);
+            }
+            i = nodes_count;
+            var middle_height = total_height / 2;
+            while (i--) {
+                node = nodes[i];
+                node._data.layout.offset_y += middle_height;
+            }
+            return total_height;
+        },
+
+        // layout the y axis only, for collapse/expand a node
+        _layout_offset_subnodes_height: function (nodes) {
+            var total_height = 0;
+            var nodes_count = nodes.length;
+            var i = nodes_count;
+            var node = null;
+            var node_outer_height = 0;
+            var layout_data = null;
+            var base_y = 0;
+            var pd = null; // parent._data
+            while (i--) {
+                node = nodes[i];
+                layout_data = node._data.layout;
+                if (pd == null) {
+                    pd = node.parent._data;
+                }
+
+                node_outer_height = this._layout_offset_subnodes_height(node.children);
+                if (!node.expanded) {
+                    node_outer_height = 0;
+                }
+                node_outer_height = Math.max(node._data.view.height, node_outer_height);
+
+                layout_data.outer_height = node_outer_height;
+                layout_data.offset_y = base_y - node_outer_height / 2;
+                base_y = base_y - node_outer_height - this.opts.vspace;
+                total_height += node_outer_height;
+            }
+            if (nodes_count > 1) {
+                total_height += this.opts.vspace * (nodes_count - 1);
+            }
+            i = nodes_count;
+            var middle_height = total_height / 2;
+            while (i--) {
+                node = nodes[i];
+                node._data.layout.offset_y += middle_height;
+                //logger.debug(node.topic);
+                //logger.debug(node._data.layout.offset_y);
+            }
+            return total_height;
+        },
+
+        get_node_offset: function (node) {
+            var layout_data = node._data.layout;
+            var offset_cache = null;
+            if (('_offset_' in layout_data) && this.cache_valid) {
+                offset_cache = layout_data._offset_;
+            } else {
+                offset_cache = { x: -1, y: -1 };
+                layout_data._offset_ = offset_cache;
+            }
+            if (offset_cache.x == -1 || offset_cache.y == -1) {
+                var x = layout_data.offset_x;
+                var y = layout_data.offset_y;
+                if (!node.isroot) {
+                    var offset_p = this.get_node_offset(node.parent);
+                    x += offset_p.x;
+                    y += offset_p.y;
+                }
+                offset_cache.x = x;
+                offset_cache.y = y;
+            }
+            return offset_cache;
+        },
+
+        get_node_point: function (node) {
+            var view_data = node._data.view;
+            var offset_p = this.get_node_offset(node);
+            //logger.debug(offset_p);
+            var p = {};
+            p.x = offset_p.x + view_data.width * (node._data.layout.direction - 1) / 2;
+            p.y = offset_p.y - view_data.height / 2;
+            //logger.debug(p);
+            return p;
+        },
+
+        get_node_point_in: function (node) {
+            var p = this.get_node_offset(node);
+            return p;
+        },
+
+        get_node_point_out: function (node) {
+            var layout_data = node._data.layout;
+            var pout_cache = null;
+            if (('_pout_' in layout_data) && this.cache_valid) {
+                pout_cache = layout_data._pout_;
+            } else {
+                pout_cache = { x: -1, y: -1 };
+                layout_data._pout_ = pout_cache;
+            }
+            if (pout_cache.x == -1 || pout_cache.y == -1) {
+                if (node.isroot) {
+                    pout_cache.x = 0;
+                    pout_cache.y = 0;
+                } else {
+                    var view_data = node._data.view;
+                    var offset_p = this.get_node_offset(node);
+                    pout_cache.x = offset_p.x + (view_data.width + this.opts.pspace) * node._data.layout.direction;
+                    pout_cache.y = offset_p.y;
+                    //logger.debug('pout');
+                    //logger.debug(pout_cache);
+                }
+            }
+            return pout_cache;
+        },
+
+        get_expander_point: function (node) {
+            var p = this.get_node_point_out(node);
+            var ex_p = {};
+            if (node._data.layout.direction == jm.direction.right) {
+                ex_p.x = p.x - this.opts.pspace;
+            } else {
+                ex_p.x = p.x;
+            }
+            ex_p.y = p.y - Math.ceil(this.opts.pspace / 2);
+            return ex_p;
+        },
+
+        get_min_size: function () {
+            var nodes = this.jm.mind.nodes;
+            var node = null;
+            var pout = null;
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                pout = this.get_node_point_out(node);
+                if (pout.x > this.bounds.e) { this.bounds.e = pout.x; }
+                if (pout.x < this.bounds.w) { this.bounds.w = pout.x; }
+            }
+            return {
+                w: this.bounds.e - this.bounds.w,
+                h: this.bounds.s - this.bounds.n
+            }
+        },
+
+        toggle_node: function (node) {
+            if (node.isroot) {
+                return;
+            }
+            if (node.expanded) {
+                this.collapse_node(node);
+            } else {
+                this.expand_node(node);
+            }
+        },
+
+        expand_node: function (node) {
+            node.expanded = true;
+            this.part_layout(node);
+            this.set_visible(node.children, true);
+            this.jm.invoke_event_handle(jm.event_type.show, { evt: 'expand_node', data: [], node: node.id });
+        },
+
+        collapse_node: function (node) {
+            node.expanded = false;
+            this.part_layout(node);
+            this.set_visible(node.children, false);
+            this.jm.invoke_event_handle(jm.event_type.show, { evt: 'collapse_node', data: [], node: node.id });
+        },
+
+        expand_all: function () {
+            var nodes = this.jm.mind.nodes;
+            var c = 0;
+            var node;
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                if (!node.expanded) {
+                    node.expanded = true;
+                    c++;
+                }
+            }
+            if (c > 0) {
+                var root = this.jm.mind.root;
+                this.part_layout(root);
+                this.set_visible(root.children, true);
+            }
+        },
+
+        collapse_all: function () {
+            var nodes = this.jm.mind.nodes;
+            var c = 0;
+            var node;
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                if (node.expanded && !node.isroot) {
+                    node.expanded = false;
+                    c++;
+                }
+            }
+            if (c > 0) {
+                var root = this.jm.mind.root;
+                this.part_layout(root);
+                this.set_visible(root.children, true);
+            }
+        },
+
+        expand_to_depth: function (target_depth, curr_nodes, curr_depth) {
+            if (target_depth < 1) { return; }
+            var nodes = curr_nodes || this.jm.mind.root.children;
+            var depth = curr_depth || 1;
+            var i = nodes.length;
+            var node = null;
+            while (i--) {
+                node = nodes[i];
+                if (depth < target_depth) {
+                    if (!node.expanded) {
+                        this.expand_node(node);
+                    }
+                    this.expand_to_depth(target_depth, node.children, depth + 1);
+                }
+                if (depth == target_depth) {
+                    if (node.expanded) {
+                        this.collapse_node(node);
+                    }
+                }
+            }
+        },
+
+        part_layout: function (node) {
+            var root = this.jm.mind.root;
+            if (!!root) {
+                var root_layout_data = root._data.layout;
+                if (node.isroot) {
+                    root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes);
+                    root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes);
+                } else {
+                    if (node._data.layout.direction == jm.direction.right) {
+                        root_layout_data.outer_height_right = this._layout_offset_subnodes_height(root_layout_data.right_nodes);
+                    } else {
+                        root_layout_data.outer_height_left = this._layout_offset_subnodes_height(root_layout_data.left_nodes);
+                    }
+                }
+                this.bounds.s = Math.max(root_layout_data.outer_height_left, root_layout_data.outer_height_right);
+                this.cache_valid = false;
+            } else {
+                logger.warn('can not found root node');
+            }
+        },
+
+        set_visible: function (nodes, visible) {
+            var i = nodes.length;
+            var node = null;
+            var layout_data = null;
+            while (i--) {
+                node = nodes[i];
+                layout_data = node._data.layout;
+                if (node.expanded) {
+                    this.set_visible(node.children, visible);
+                } else {
+                    this.set_visible(node.children, false);
+                }
+                if (!node.isroot) {
+                    node._data.layout.visible = visible;
+                }
+            }
+        },
+
+        is_expand: function (node) {
+            return node.expanded;
+        },
+
+        is_visible: function (node) {
+            var layout_data = node._data.layout;
+            if (('visible' in layout_data) && !layout_data.visible) {
+                return false;
+            } else {
+                return true;
+            }
+        }
+    };
+
+    jm.graph_canvas = function (view) {
+        this.opts = view.opts;
+        this.e_canvas = $c('canvas');
+        this.e_canvas.className = 'jsmind';
+        this.canvas_ctx = this.e_canvas.getContext('2d');
+        this.size = { w: 0, h: 0 };
+    };
+
+    jm.graph_canvas.prototype = {
+        element: function () {
+            return this.e_canvas;
+        },
+
+        set_size: function (w, h) {
+            this.size.w = w;
+            this.size.h = h;
+            this.e_canvas.width = w;
+            this.e_canvas.height = h;
+        },
+
+        clear: function () {
+            this.canvas_ctx.clearRect(0, 0, this.size.w, this.size.h);
+        },
+
+        draw_line: function (pout, pin, offset) {
+            var ctx = this.canvas_ctx;
+            ctx.strokeStyle = this.opts.line_color;
+            ctx.lineWidth = this.opts.line_width;
+            ctx.lineCap = 'round';
+
+            this._bezier_to(ctx,
+                pin.x + offset.x,
+                pin.y + offset.y,
+                pout.x + offset.x,
+                pout.y + offset.y);
+        },
+
+        copy_to: function (dest_canvas_ctx, callback) {
+            dest_canvas_ctx.drawImage(this.e_canvas, 0, 0);
+            !!callback && callback();
+        },
+
+        _bezier_to: function (ctx, x1, y1, x2, y2) {
+            ctx.beginPath();
+            ctx.moveTo(x1, y1);
+            ctx.bezierCurveTo(x1 + (x2 - x1) * 2 / 3, y1, x1, y2, x2, y2);
+            ctx.stroke();
+        },
+
+        _line_to: function (ctx, x1, y1, x2, y2) {
+            ctx.beginPath();
+            ctx.moveTo(x1, y1);
+            ctx.lineTo(x2, y2);
+            ctx.stroke();
+        }
+    };
+
+    jm.graph_svg = function (view) {
+        this.view = view;
+        this.opts = view.opts;
+        this.e_svg = jm.graph_svg.c('svg');
+        this.e_svg.setAttribute('class', 'jsmind');
+        this.size = { w: 0, h: 0 };
+        this.lines = [];
+    };
+
+    jm.graph_svg.c = function (tag) {
+        return $d.createElementNS('http://www.w3.org/2000/svg', tag);
+    };
+
+    jm.graph_svg.prototype = {
+        element: function () {
+            return this.e_svg;
+        },
+
+        set_size: function (w, h) {
+            this.size.w = w;
+            this.size.h = h;
+            this.e_svg.setAttribute('width', w);
+            this.e_svg.setAttribute('height', h);
+        },
+
+        clear: function () {
+            var len = this.lines.length;
+            while (len--) {
+                this.e_svg.removeChild(this.lines[len]);
+            }
+            this.lines.length = 0;
+        },
+
+        draw_line: function (pout, pin, offset) {
+            var line = jm.graph_svg.c('path');
+            line.setAttribute('stroke', this.opts.line_color);
+            line.setAttribute('stroke-width', this.opts.line_width);
+            line.setAttribute('fill', 'transparent');
+            this.lines.push(line);
+            this.e_svg.appendChild(line);
+            this._bezier_to(line, pin.x + offset.x, pin.y + offset.y, pout.x + offset.x, pout.y + offset.y);
+        },
+
+        copy_to: function (dest_canvas_ctx, callback) {
+            var img = new Image();
+            img.onload = function () {
+                dest_canvas_ctx.drawImage(img, 0, 0);
+                !!callback && callback();
+            }
+            img.src = 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(this.e_svg));
+        },
+
+        _bezier_to: function (path, x1, y1, x2, y2) {
+            path.setAttribute('d', 'M' + x1 + ' ' + y1 + ' C ' + (x1 + (x2 - x1) * 2 / 3) + ' ' + y1 + ', ' + x1 + ' ' + y2 + ', ' + x2 + ' ' + y2);
+        },
+
+        _line_to: function (path, x1, y1, x2, y2) {
+            path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2);
+        }
+    };
+
+    // view provider
+    jm.view_provider = function (jm, options) {
+        this.opts = options;
+        this.jm = jm;
+        this.layout = jm.layout;
+
+        this.container = null;
+        this.e_panel = null;
+        this.e_nodes = null;
+
+        this.size = { w: 0, h: 0 };
+
+        this.selected_node = null;
+        this.editing_node = null;
+
+        this.graph = null;
+    };
+
+    jm.view_provider.prototype = {
+        init: function () {
+            logger.debug('view.init');
+
+            this.container = $i(this.opts.container) ? this.opts.container : $g(this.opts.container);
+            if (!this.container) {
+                logger.error('the options.view.container was not be found in dom');
+                return;
+            }
+            this.e_panel = $c('div');
+            this.e_nodes = $c('jmnodes');
+            this.e_editor = $c('input');
+
+            this.graph = this.opts.engine.toLowerCase() === 'svg' ? new jm.graph_svg(this) : new jm.graph_canvas(this);
+
+            this.e_panel.className = 'jsmind-inner';
+            this.e_panel.tabIndex = 1;
+            this.e_panel.appendChild(this.graph.element());
+            this.e_panel.appendChild(this.e_nodes);
+
+            this.e_editor.className = 'jsmind-editor';
+            this.e_editor.type = 'text';
+
+            this.actualZoom = 1;
+            this.zoomStep = 0.1;
+            this.minZoom = 0.5;
+            this.maxZoom = 2;
+
+            var v = this;
+            jm.util.dom.add_event(this.e_editor, 'keydown', function (e) {
+                var evt = e || event;
+                if (evt.keyCode == 13) { v.edit_node_end(); evt.stopPropagation(); }
+            });
+            jm.util.dom.add_event(this.e_editor, 'blur', function (e) {
+                v.edit_node_end();
+            });
+
+            this.container.appendChild(this.e_panel);
+        },
+
+        add_event: function (obj, event_name, event_handle) {
+            jm.util.dom.add_event(this.e_nodes, event_name, function (e) {
+                var evt = e || event;
+                event_handle.call(obj, evt);
+            });
+        },
+
+        get_binded_nodeid: function (element) {
+            if (element == null) {
+                return null;
+            }
+            var tagName = element.tagName.toLowerCase();
+            if (tagName == 'jmnodes' || tagName == 'body' || tagName == 'html') {
+                return null;
+            }
+            if (tagName == 'jmnode' || tagName == 'jmexpander') {
+                return element.getAttribute('nodeid');
+            } else {
+                return this.get_binded_nodeid(element.parentElement);
+            }
+        },
+
+        is_expander: function (element) {
+            return (element.tagName.toLowerCase() == 'jmexpander');
+        },
+
+        reset: function () {
+            logger.debug('view.reset');
+            this.selected_node = null;
+            this.clear_lines();
+            this.clear_nodes();
+            this.reset_theme();
+        },
+
+        reset_theme: function () {
+            var theme_name = this.jm.options.theme;
+            if (!!theme_name) {
+                this.e_nodes.className = 'theme-' + theme_name;
+            } else {
+                this.e_nodes.className = '';
+            }
+        },
+
+        reset_custom_style: function () {
+            var nodes = this.jm.mind.nodes;
+            for (var nodeid in nodes) {
+                this.reset_node_custom_style(nodes[nodeid]);
+            }
+        },
+
+        load: function () {
+            logger.debug('view.load');
+            this.init_nodes();
+        },
+
+        expand_size: function () {
+            var min_size = this.layout.get_min_size();
+            var min_width = min_size.w + this.opts.hmargin * 2;
+            var min_height = min_size.h + this.opts.vmargin * 2;
+            var client_w = this.e_panel.clientWidth;
+            var client_h = this.e_panel.clientHeight;
+            if (client_w < min_width) { client_w = min_width; }
+            if (client_h < min_height) { client_h = min_height; }
+            this.size.w = client_w;
+            this.size.h = client_h;
+        },
+
+        init_nodes_size: function (node) {
+            var view_data = node._data.view;
+            view_data.width = view_data.element.clientWidth;
+            view_data.height = view_data.element.clientHeight;
+        },
+
+        init_nodes: function () {
+            var nodes = this.jm.mind.nodes;
+            var doc_frag = $d.createDocumentFragment();
+            for (var nodeid in nodes) {
+                this.create_node_element(nodes[nodeid], doc_frag);
+            }
+            this.e_nodes.appendChild(doc_frag);
+            for (var nodeid in nodes) {
+                this.init_nodes_size(nodes[nodeid]);
+            }
+        },
+
+        add_node: function (node) {
+            this.create_node_element(node, this.e_nodes);
+            this.init_nodes_size(node);
+        },
+
+        create_node_element: function (node, parent_node) {
+            var view_data = null;
+            if ('view' in node._data) {
+                view_data = node._data.view;
+            } else {
+                view_data = {};
+                node._data.view = view_data;
+            }
+
+            var d = $c('jmnode');
+            if (node.isroot) {
+                d.className = 'root';
+            } else {
+                var d_e = $c('jmexpander');
+                $t(d_e, '-');
+                d_e.setAttribute('nodeid', node.id);
+                d_e.style.visibility = 'hidden';
+                parent_node.appendChild(d_e);
+                view_data.expander = d_e;
+            }
+            if (!!node.topic) {
+                if (this.opts.support_html) {
+                    $h(d, node.topic);
+                } else {
+                    $t(d, node.topic);
+                }
+            }
+            d.setAttribute('nodeid', node.id);
+            d.style.visibility = 'hidden';
+            this._reset_node_custom_style(d, node.data);
+
+            parent_node.appendChild(d);
+            view_data.element = d;
+        },
+
+        remove_node: function (node) {
+            if (this.selected_node != null && this.selected_node.id == node.id) {
+                this.selected_node = null;
+            }
+            if (this.editing_node != null && this.editing_node.id == node.id) {
+                node._data.view.element.removeChild(this.e_editor);
+                this.editing_node = null;
+            }
+            var children = node.children;
+            var i = children.length;
+            while (i--) {
+                this.remove_node(children[i]);
+            }
+            if (node._data.view) {
+                var element = node._data.view.element;
+                var expander = node._data.view.expander;
+                this.e_nodes.removeChild(element);
+                this.e_nodes.removeChild(expander);
+                node._data.view.element = null;
+                node._data.view.expander = null;
+            }
+        },
+
+        update_node: function (node) {
+            var view_data = node._data.view;
+            var element = view_data.element;
+            if (!!node.topic) {
+                if (this.opts.support_html) {
+                    $h(element, node.topic);
+                } else {
+                    $t(element, node.topic);
+                }
+            }
+            view_data.width = element.clientWidth;
+            view_data.height = element.clientHeight;
+        },
+
+        select_node: function (node) {
+            if (!!this.selected_node) {
+                this.selected_node._data.view.element.className =
+                    this.selected_node._data.view.element.className.replace(/\s*selected\b/i, '');
+                this.reset_node_custom_style(this.selected_node);
+            }
+            if (!!node) {
+                this.selected_node = node;
+                node._data.view.element.className += ' selected';
+                this.clear_node_custom_style(node);
+            }
+        },
+
+        select_clear: function () {
+            this.select_node(null);
+        },
+
+        get_editing_node: function () {
+            return this.editing_node;
+        },
+
+        is_editing: function () {
+            return (!!this.editing_node);
+        },
+
+        edit_node_begin: function (node) {
+            if (!node.topic) {
+                logger.warn("don't edit image nodes");
+                return;
+            }
+            if (this.editing_node != null) {
+                this.edit_node_end();
+            }
+            this.editing_node = node;
+            var view_data = node._data.view;
+            var element = view_data.element;
+            var topic = node.topic;
+            var ncs = getComputedStyle(element);
+            this.e_editor.value = topic;
+            this.e_editor.style.width = (element.clientWidth - parseInt(ncs.getPropertyValue('padding-left')) - parseInt(ncs.getPropertyValue('padding-right'))) + 'px';
+            element.innerHTML = '';
+            element.appendChild(this.e_editor);
+            element.style.zIndex = 5;
+            this.e_editor.focus();
+            this.e_editor.select();
+        },
+
+        edit_node_end: function () {
+            if (this.editing_node != null) {
+                var node = this.editing_node;
+                this.editing_node = null;
+                var view_data = node._data.view;
+                var element = view_data.element;
+                var topic = this.e_editor.value;
+                element.style.zIndex = 'auto';
+                element.removeChild(this.e_editor);
+                if (jm.util.text.is_empty(topic) || node.topic === topic) {
+                    if (this.opts.support_html) {
+                        $h(element, node.topic);
+                    } else {
+                        $t(element, node.topic);
+                    }
+                } else {
+                    this.jm.update_node(node.id, topic);
+                }
+            }
+        },
+
+        get_view_offset: function () {
+            var bounds = this.layout.bounds;
+            var _x = (this.size.w - bounds.e - bounds.w) / 2;
+            var _y = this.size.h / 2;
+            return { x: _x, y: _y };
+        },
+
+        resize: function () {
+            this.graph.set_size(1, 1);
+            this.e_nodes.style.width = '1px';
+            this.e_nodes.style.height = '1px';
+
+            this.expand_size();
+            this._show();
+        },
+
+        _show: function () {
+            this.graph.set_size(this.size.w, this.size.h);
+            this.e_nodes.style.width = this.size.w + 'px';
+            this.e_nodes.style.height = this.size.h + 'px';
+            this.show_nodes();
+            this.show_lines();
+            //this.layout.cache_valid = true;
+            this.jm.invoke_event_handle(jm.event_type.resize, { data: [] });
+        },
+
+        zoomIn: function () {
+            return this.setZoom(this.actualZoom + this.zoomStep);
+        },
+
+        zoomOut: function () {
+            return this.setZoom(this.actualZoom - this.zoomStep);
+        },
+
+        setZoom: function (zoom) {
+            if ((zoom < this.minZoom) || (zoom > this.maxZoom)) {
+                return false;
+            }
+            this.actualZoom = zoom;
+            for (var i = 0; i < this.e_panel.children.length; i++) {
+                this.e_panel.children[i].style.transform = 'scale(' + zoom + ')';
+            };
+            this.show(true);
+            return true;
+
+        },
+
+        _center_root: function () {
+            // center root node
+            var outer_w = this.e_panel.clientWidth;
+            var outer_h = this.e_panel.clientHeight;
+            if (this.size.w > outer_w) {
+                var _offset = this.get_view_offset();
+                this.e_panel.scrollLeft = _offset.x - outer_w / 2;
+            }
+            if (this.size.h > outer_h) {
+                this.e_panel.scrollTop = (this.size.h - outer_h) / 2;
+            }
+        },
+
+        show: function (keep_center) {
+            logger.debug('view.show');
+            this.expand_size();
+            this._show();
+            if (!!keep_center) {
+                this._center_root();
+            }
+        },
+
+        relayout: function () {
+            this.expand_size();
+            this._show();
+        },
+
+        save_location: function (node) {
+            var vd = node._data.view;
+            vd._saved_location = {
+                x: parseInt(vd.element.style.left) - this.e_panel.scrollLeft,
+                y: parseInt(vd.element.style.top) - this.e_panel.scrollTop,
+            };
+        },
+
+        restore_location: function (node) {
+            var vd = node._data.view;
+            this.e_panel.scrollLeft = parseInt(vd.element.style.left) - vd._saved_location.x;
+            this.e_panel.scrollTop = parseInt(vd.element.style.top) - vd._saved_location.y;
+        },
+
+        clear_nodes: function () {
+            var mind = this.jm.mind;
+            if (mind == null) {
+                return;
+            }
+            var nodes = mind.nodes;
+            var node = null;
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                node._data.view.element = null;
+                node._data.view.expander = null;
+            }
+            this.e_nodes.innerHTML = '';
+        },
+
+        show_nodes: function () {
+            var nodes = this.jm.mind.nodes;
+            var node = null;
+            var node_element = null;
+            var expander = null;
+            var p = null;
+            var p_expander = null;
+            var expander_text = '-';
+            var view_data = null;
+            var _offset = this.get_view_offset();
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                view_data = node._data.view;
+                node_element = view_data.element;
+                expander = view_data.expander;
+                if (!this.layout.is_visible(node)) {
+                    node_element.style.display = 'none';
+                    expander.style.display = 'none';
+                    continue;
+                }
+                this.reset_node_custom_style(node);
+                p = this.layout.get_node_point(node);
+                view_data.abs_x = _offset.x + p.x;
+                view_data.abs_y = _offset.y + p.y;
+                node_element.style.left = (_offset.x + p.x) + 'px';
+                node_element.style.top = (_offset.y + p.y) + 'px';
+                node_element.style.display = '';
+                node_element.style.visibility = 'visible';
+                if (!node.isroot && node.children.length > 0) {
+                    expander_text = node.expanded ? '-' : '+';
+                    p_expander = this.layout.get_expander_point(node);
+                    expander.style.left = (_offset.x + p_expander.x) + 'px';
+                    expander.style.top = (_offset.y + p_expander.y) + 'px';
+                    expander.style.display = '';
+                    expander.style.visibility = 'visible';
+                    $t(expander, expander_text);
+                }
+                // hide expander while all children have been removed
+                if (!node.isroot && node.children.length == 0) {
+                    expander.style.display = 'none';
+                    expander.style.visibility = 'hidden';
+                }
+            }
+        },
+
+        reset_node_custom_style: function (node) {
+            this._reset_node_custom_style(node._data.view.element, node.data);
+        },
+
+        _reset_node_custom_style: function (node_element, node_data) {
+            if ('background-color' in node_data) {
+                node_element.style.backgroundColor = node_data['background-color'];
+            }
+            if ('foreground-color' in node_data) {
+                node_element.style.color = node_data['foreground-color'];
+            }
+            if ('width' in node_data) {
+                node_element.style.width = node_data['width'] + 'px';
+            }
+            if ('height' in node_data) {
+                node_element.style.height = node_data['height'] + 'px';
+            }
+            if ('font-size' in node_data) {
+                node_element.style.fontSize = node_data['font-size'] + 'px';
+            }
+            if ('font-weight' in node_data) {
+                node_element.style.fontWeight = node_data['font-weight'];
+            }
+            if ('font-style' in node_data) {
+                node_element.style.fontStyle = node_data['font-style'];
+            }
+            if ('background-image' in node_data) {
+                var backgroundImage = node_data['background-image'];
+                if (backgroundImage.startsWith('data') && node_data['width'] && node_data['height']) {
+                    var img = new Image();
+
+                    img.onload = function () {
+                        var c = $c('canvas');
+                        c.width = node_element.clientWidth;
+                        c.height = node_element.clientHeight;
+                        var img = this;
+                        if (c.getContext) {
+                            var ctx = c.getContext('2d');
+                            ctx.drawImage(img, 2, 2, node_element.clientWidth, node_element.clientHeight);
+                            var scaledImageData = c.toDataURL();
+                            node_element.style.backgroundImage = 'url(' + scaledImageData + ')';
+                        }
+                    };
+                    img.src = backgroundImage;
+
+                } else {
+                    node_element.style.backgroundImage = 'url(' + backgroundImage + ')';
+                }
+                node_element.style.backgroundSize = '99%';
+
+                if ('background-rotation' in node_data) {
+                    node_element.style.transform = 'rotate(' + node_data['background-rotation'] + 'deg)';
+                }
+            }
+        },
+
+        clear_node_custom_style: function (node) {
+            var node_element = node._data.view.element;
+            node_element.style.backgroundColor = "";
+            node_element.style.color = "";
+        },
+
+        clear_lines: function () {
+            this.graph.clear();
+        },
+
+        show_lines: function () {
+            this.clear_lines();
+            var nodes = this.jm.mind.nodes;
+            var node = null;
+            var pin = null;
+            var pout = null;
+            var _offset = this.get_view_offset();
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                if (!!node.isroot) { continue; }
+                if (('visible' in node._data.layout) && !node._data.layout.visible) { continue; }
+                pin = this.layout.get_node_point_in(node);
+                pout = this.layout.get_node_point_out(node.parent);
+                this.graph.draw_line(pout, pin, _offset);
+            }
+        },
+    };
+
+    // shortcut provider
+    jm.shortcut_provider = function (jm, options) {
+        this.jm = jm;
+        this.opts = options;
+        this.mapping = options.mapping;
+        this.handles = options.handles;
+        this._newid = null;
+        this._mapping = {};
+    };
+
+    jm.shortcut_provider.prototype = {
+        init: function () {
+            jm.util.dom.add_event(this.jm.view.e_panel, 'keydown', this.handler.bind(this));
+
+            this.handles['addchild'] = this.handle_addchild;
+            this.handles['addbrother'] = this.handle_addbrother;
+            this.handles['editnode'] = this.handle_editnode;
+            this.handles['delnode'] = this.handle_delnode;
+            this.handles['toggle'] = this.handle_toggle;
+            this.handles['up'] = this.handle_up;
+            this.handles['down'] = this.handle_down;
+            this.handles['left'] = this.handle_left;
+            this.handles['right'] = this.handle_right;
+
+            for (var handle in this.mapping) {
+                if (!!this.mapping[handle] && (handle in this.handles)) {
+                    this._mapping[this.mapping[handle]] = this.handles[handle];
+                }
+            }
+
+            if (typeof this.opts.id_generator === 'function') {
+                this._newid = this.opts.id_generator;
+            } else {
+                this._newid = jm.util.uuid.newid;
+            }
+        },
+
+        enable_shortcut: function () {
+            this.opts.enable = true;
+        },
+
+        disable_shortcut: function () {
+            this.opts.enable = false;
+        },
+
+        handler: function (e) {
+            if (e.which == 9) { e.preventDefault(); } //prevent tab to change focus in browser
+            if (this.jm.view.is_editing()) { return; }
+            var evt = e || event;
+            if (!this.opts.enable) { return true; }
+            var kc = evt.keyCode + (evt.metaKey << 13) + (evt.ctrlKey << 12) + (evt.altKey << 11) + (evt.shiftKey << 10);
+            if (kc in this._mapping) {
+                this._mapping[kc].call(this, this.jm, e);
+            }
+        },
+
+        handle_addchild: function (_jm, e) {
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node) {
+                var nodeid = this._newid();
+                var node = _jm.add_node(selected_node, nodeid, 'New Node');
+                if (!!node) {
+                    _jm.select_node(nodeid);
+                    _jm.begin_edit(nodeid);
+                }
+            }
+        },
+        handle_addbrother: function (_jm, e) {
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node && !selected_node.isroot) {
+                var nodeid = this._newid();
+                var node = _jm.insert_node_after(selected_node, nodeid, 'New Node');
+                if (!!node) {
+                    _jm.select_node(nodeid);
+                    _jm.begin_edit(nodeid);
+                }
+            }
+        },
+        handle_editnode: function (_jm, e) {
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node) {
+                _jm.begin_edit(selected_node);
+            }
+        },
+        handle_delnode: function (_jm, e) {
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node && !selected_node.isroot) {
+                _jm.select_node(selected_node.parent);
+                _jm.remove_node(selected_node);
+            }
+        },
+        handle_toggle: function (_jm, e) {
+            var evt = e || event;
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node) {
+                _jm.toggle_node(selected_node.id);
+                evt.stopPropagation();
+                evt.preventDefault();
+            }
+        },
+        handle_up: function (_jm, e) {
+            var evt = e || event;
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node) {
+                var up_node = _jm.find_node_before(selected_node);
+                if (!up_node) {
+                    var np = _jm.find_node_before(selected_node.parent);
+                    if (!!np && np.children.length > 0) {
+                        up_node = np.children[np.children.length - 1];
+                    }
+                }
+                if (!!up_node) {
+                    _jm.select_node(up_node);
+                }
+                evt.stopPropagation();
+                evt.preventDefault();
+            }
+        },
+
+        handle_down: function (_jm, e) {
+            var evt = e || event;
+            var selected_node = _jm.get_selected_node();
+            if (!!selected_node) {
+                var down_node = _jm.find_node_after(selected_node);
+                if (!down_node) {
+                    var np = _jm.find_node_after(selected_node.parent);
+                    if (!!np && np.children.length > 0) {
+                        down_node = np.children[0];
+                    }
+                }
+                if (!!down_node) {
+                    _jm.select_node(down_node);
+                }
+                evt.stopPropagation();
+                evt.preventDefault();
+            }
+        },
+
+        handle_left: function (_jm, e) {
+            this._handle_direction(_jm, e, jm.direction.left);
+        },
+        handle_right: function (_jm, e) {
+            this._handle_direction(_jm, e, jm.direction.right);
+        },
+        _handle_direction: function (_jm, e, d) {
+            var evt = e || event;
+            var selected_node = _jm.get_selected_node();
+            var node = null;
+            if (!!selected_node) {
+                if (selected_node.isroot) {
+                    var c = selected_node.children;
+                    var children = [];
+                    for (var i = 0; i < c.length; i++) {
+                        if (c[i].direction === d) {
+                            children.push(i);
+                        }
+                    }
+                    node = c[children[Math.floor((children.length - 1) / 2)]];
+                }
+                else if (selected_node.direction === d) {
+                    var children = selected_node.children;
+                    var childrencount = children.length;
+                    if (childrencount > 0) {
+                        node = children[Math.floor((childrencount - 1) / 2)];
+                    }
+                } else {
+                    node = selected_node.parent;
+                }
+                if (!!node) {
+                    _jm.select_node(node);
+                }
+                evt.stopPropagation();
+                evt.preventDefault();
+            }
+        },
+    };
+
+
+    // plugin
+    jm.plugin = function (name, init) {
+        this.name = name;
+        this.init = init;
+    };
+
+    jm.plugins = [];
+
+    jm.register_plugin = function (plugin) {
+        if (plugin instanceof jm.plugin) {
+            jm.plugins.push(plugin);
+        }
+    };
+
+    jm.init_plugins = function (sender) {
+        $w.setTimeout(function () {
+            jm._init_plugins(sender);
+        }, 0);
+    };
+
+    jm._init_plugins = function (sender) {
+        var l = jm.plugins.length;
+        var fn_init = null;
+        for (var i = 0; i < l; i++) {
+            fn_init = jm.plugins[i].init;
+            if (typeof fn_init === 'function') {
+                fn_init(sender);
+            }
+        }
+    };
+
+    // quick way
+    jm.show = function (options, mind) {
+        var _jm = new jm(options);
+        _jm.show(mind);
+        return _jm;
+    };
+
+    // export jsmind
+    if (typeof module !== 'undefined' && typeof exports === 'object') {
+        module.exports = jm;
+    } else if (typeof define === 'function' && (define.amd || define.cmd)) {
+        define(function () { return jm; });
+    } else {
+        $w[__name__] = jm;
+    }
+})(typeof window !== 'undefined' ? window : global);
+

+ 351 - 0
jsmind/js/jsmind.screenshot.js

@@ -0,0 +1,351 @@
+/*
+ * Released under BSD License
+ * Copyright (c) 2014-2021 hizzgdev@163.com
+ * 
+ * Project Home:
+ *   https://github.com/hizzgdev/jsmind/
+ */
+
+(function ($w) {
+    'use strict';
+
+    var __name__ = 'jsMind';
+    var jsMind = $w[__name__];
+    if (!jsMind) { return; }
+    if (typeof jsMind.screenshot != 'undefined') { return; }
+
+    var $d = $w.document;
+    var $c = function (tag) { return $d.createElement(tag); };
+
+    var css = function (cstyle, property_name) {
+        return cstyle.getPropertyValue(property_name);
+    };
+    var is_visible = function (cstyle) {
+        var visibility = css(cstyle, 'visibility');
+        var display = css(cstyle, 'display');
+        return (visibility !== 'hidden' && display !== 'none');
+    };
+    var jcanvas = {};
+    jcanvas.rect = function (ctx, x, y, w, h, r) {
+        if (w < 2 * r) r = w / 2;
+        if (h < 2 * r) r = h / 2;
+        ctx.moveTo(x + r, y);
+        ctx.arcTo(x + w, y, x + w, y + h, r);
+        ctx.arcTo(x + w, y + h, x, y + h, r);
+        ctx.arcTo(x, y + h, x, y, r);
+        ctx.arcTo(x, y, x + w, y, r);
+    };
+
+    jcanvas.text_multiline = function (ctx, text, x, y, w, h, lineheight) {
+        var line = '';
+        var text_len = text.length;
+        var chars = text.split('');
+        var test_line = null;
+        ctx.textAlign = 'left';
+        ctx.textBaseline = 'top';
+        for (var i = 0; i < text_len; i++) {
+            test_line = line + chars[i];
+            if (ctx.measureText(test_line).width > w && i > 0) {
+                ctx.fillText(line, x, y);
+                line = chars[i];
+                y += lineheight;
+            } else {
+                line = test_line;
+            }
+        }
+        ctx.fillText(line, x, y);
+    };
+
+    jcanvas.text_ellipsis = function (ctx, text, x, y, w, h) {
+        var center_y = y + h / 2;
+        var text = jcanvas.fittingString(ctx, text, w);
+        ctx.textAlign = 'left';
+        ctx.textBaseline = 'middle';
+        ctx.fillText(text, x, center_y, w);
+    };
+
+    jcanvas.fittingString = function (ctx, text, max_width) {
+        var width = ctx.measureText(text).width;
+        var ellipsis = '…';
+        var ellipsis_width = ctx.measureText(ellipsis).width;
+        if (width <= max_width || width <= ellipsis_width) {
+            return text;
+        } else {
+            var len = text.length;
+            while (width >= max_width - ellipsis_width && len-- > 0) {
+                text = text.substring(0, len);
+                width = ctx.measureText(text).width;
+            }
+            return text + ellipsis;
+        }
+    };
+
+    jcanvas.image = function (ctx, url, x, y, w, h, r, rotation, callback) {
+        var img = new Image();
+        img.onload = function () {
+            ctx.save();
+            ctx.translate(x, y);
+            ctx.save();
+            ctx.beginPath();
+            jcanvas.rect(ctx, 0, 0, w, h, r);
+            ctx.closePath();
+            ctx.clip();
+            ctx.translate(w / 2, h / 2);
+            ctx.rotate(rotation * Math.PI / 180);
+            ctx.drawImage(img, -w / 2, -h / 2);
+            ctx.restore();
+            ctx.restore();
+            !!callback && callback();
+        }
+        img.src = url;
+    };
+
+    jsMind.screenshot = function (jm) {
+        this.jm = jm;
+        this.canvas_elem = null;
+        this.canvas_ctx = null;
+        this._inited = false;
+    };
+
+    jsMind.screenshot.prototype = {
+        init: function () {
+            if (this._inited) { return; }
+            console.log('init');
+            var c = $c('canvas');
+            var ctx = c.getContext('2d');
+
+            this.canvas_elem = c;
+            this.canvas_ctx = ctx;
+            this.jm.view.e_panel.appendChild(c);
+            this._inited = true;
+            this.resize();
+        },
+
+        shoot: function (callback) {
+            this.init();
+            this._draw(function () {
+                !!callback && callback();
+                this.clean();
+            }.bind(this));
+            this._watermark();
+        },
+
+        shootDownload: function () {
+            this.shoot(function () {
+                this._download();
+            }.bind(this));
+        },
+
+        shootAsDataURL: function (callback) {
+            this.shoot(function () {
+                !!callback && callback(this.canvas_elem.toDataURL());
+            }.bind(this));
+        },
+
+        resize: function () {
+            if (this._inited) {
+                this.canvas_elem.width = this.jm.view.size.w;
+                this.canvas_elem.height = this.jm.view.size.h;
+            }
+        },
+
+        clean: function () {
+            var c = this.canvas_elem;
+            this.canvas_ctx.clearRect(0, 0, c.width, c.height);
+        },
+
+        _draw: function (callback) {
+            var ctx = this.canvas_ctx;
+            ctx.textAlign = 'left';
+            ctx.textBaseline = 'top';
+            this._draw_lines(function () {
+                this._draw_nodes(callback);
+            }.bind(this));
+        },
+
+        _watermark: function () {
+            var c = this.canvas_elem;
+            var ctx = this.canvas_ctx;
+            ctx.textAlign = 'right';
+            ctx.textBaseline = 'bottom';
+            ctx.fillStyle = '#000';
+            ctx.font = '11px Verdana,Arial,Helvetica,sans-serif';
+            ctx.fillText('hizzgdev.github.io/jsmind', c.width - 5.5, c.height - 2.5);
+            ctx.textAlign = 'left';
+            ctx.fillText($w.location, 5.5, c.height - 2.5);
+        },
+
+        _draw_lines: function (callback) {
+            this.jm.view.graph.copy_to(this.canvas_ctx, callback);
+        },
+
+        _draw_nodes: function (callback) {
+            var nodes = this.jm.mind.nodes;
+            var node;
+            for (var nodeid in nodes) {
+                node = nodes[nodeid];
+                this._draw_node(node);
+            }
+
+            function check_nodes_ready() {
+                console.log('check_node_ready' + new Date());
+                var allOk = true;
+                for (var nodeid in nodes) {
+                    node = nodes[nodeid];
+                    allOk = allOk & node.ready;
+                }
+
+                if (!allOk) {
+                    $w.setTimeout(check_nodes_ready, 200);
+                } else {
+                    $w.setTimeout(callback, 200);
+                }
+            }
+            check_nodes_ready();
+        },
+
+        _draw_node: function (node) {
+            var ctx = this.canvas_ctx;
+            var view_data = node._data.view;
+            var node_element = view_data.element;
+            var ncs = getComputedStyle(node_element);
+            if (!is_visible(ncs)) {
+                node.ready = true;
+                return;
+            }
+
+            var bgcolor = css(ncs, 'background-color');
+            var round_radius = parseInt(css(ncs, 'border-top-left-radius'));
+            var color = css(ncs, 'color');
+            var padding_left = parseInt(css(ncs, 'padding-left'));
+            var padding_right = parseInt(css(ncs, 'padding-right'));
+            var padding_top = parseInt(css(ncs, 'padding-top'));
+            var padding_bottom = parseInt(css(ncs, 'padding-bottom'));
+            var text_overflow = css(ncs, 'text-overflow');
+            var font = css(ncs, 'font-style') + ' ' +
+                css(ncs, 'font-variant') + ' ' +
+                css(ncs, 'font-weight') + ' ' +
+                css(ncs, 'font-size') + '/' + css(ncs, 'line-height') + ' ' +
+                css(ncs, 'font-family');
+
+            var rb = {
+                x: view_data.abs_x,
+                y: view_data.abs_y,
+                w: view_data.width + 1,
+                h: view_data.height + 1
+            };
+            var tb = {
+                x: rb.x + padding_left,
+                y: rb.y + padding_top,
+                w: rb.w - padding_left - padding_right,
+                h: rb.h - padding_top - padding_bottom
+            };
+
+            ctx.font = font;
+            ctx.fillStyle = bgcolor;
+            ctx.beginPath();
+            jcanvas.rect(ctx, rb.x, rb.y, rb.w, rb.h, round_radius);
+            ctx.closePath();
+            ctx.fill();
+
+            ctx.fillStyle = color;
+            if ('background-image' in node.data) {
+                var backgroundUrl = css(ncs, 'background-image').slice(5, -2);
+                node.ready = false;
+                var rotation = 0;
+                if ('background-rotation' in node.data) {
+                    rotation = node.data['background-rotation'];
+                }
+                jcanvas.image(ctx, backgroundUrl, rb.x, rb.y, rb.w, rb.h, round_radius, rotation,
+                    function () {
+                        node.ready = true;
+                    });
+            }
+            if (!!node.topic) {
+                if (text_overflow === 'ellipsis') {
+                    jcanvas.text_ellipsis(ctx, node.topic, tb.x, tb.y, tb.w, tb.h);
+                } else {
+                    var line_height = parseInt(css(ncs, 'line-height'));
+                    jcanvas.text_multiline(ctx, node.topic, tb.x, tb.y, tb.w, tb.h, line_height);
+                }
+            }
+            if (!!view_data.expander) {
+                this._draw_expander(view_data.expander);
+            }
+            if (!('background-image' in node.data)) {
+                node.ready = true;
+            }
+        },
+
+        _draw_expander: function (expander) {
+            var ctx = this.canvas_ctx;
+            var ncs = getComputedStyle(expander);
+            if (!is_visible(ncs)) { return; }
+
+            var style_left = css(ncs, 'left');
+            var style_top = css(ncs, 'top');
+            var font = css(ncs, 'font');
+            var left = parseInt(style_left);
+            var top = parseInt(style_top);
+            var is_plus = expander.innerHTML === '+';
+
+            ctx.lineWidth = 1;
+
+            ctx.beginPath();
+            ctx.arc(left + 7, top + 7, 5, 0, Math.PI * 2, true);
+            ctx.moveTo(left + 10, top + 7);
+            ctx.lineTo(left + 4, top + 7);
+            if (is_plus) {
+                ctx.moveTo(left + 7, top + 4);
+                ctx.lineTo(left + 7, top + 10);
+            }
+            ctx.closePath();
+            ctx.stroke();
+        },
+
+        _download: function () {
+            var c = this.canvas_elem;
+            var name = this.jm.mind.name + '.png';
+
+            if (navigator.msSaveBlob && (!!c.msToBlob)) {
+                var blob = c.msToBlob();
+                navigator.msSaveBlob(blob, name);
+            } else {
+                var bloburl = this.canvas_elem.toDataURL();
+                var anchor = $c('a');
+                if ('download' in anchor) {
+                    anchor.style.visibility = 'hidden';
+                    anchor.href = bloburl;
+                    anchor.download = name;
+                    $d.body.appendChild(anchor);
+                    var evt = $d.createEvent('MouseEvents');
+                    evt.initEvent('click', true, true);
+                    anchor.dispatchEvent(evt);
+                    $d.body.removeChild(anchor);
+                } else {
+                    location.href = bloburl;
+                }
+            }
+        },
+
+        jm_event_handle: function (type, data) {
+            if (type === jsMind.event_type.resize) {
+                this.resize();
+            }
+        }
+    };
+
+    var screenshot_plugin = new jsMind.plugin('screenshot', function (jm) {
+        var jss = new jsMind.screenshot(jm);
+        jm.screenshot = jss;
+        jm.shoot = function () {
+            jss.shoot();
+        };
+        jm.add_event_listener(function (type, data) {
+            jss.jm_event_handle.call(jss, type, data);
+        });
+    });
+
+    jsMind.register_plugin(screenshot_plugin);
+
+})(window);

+ 34 - 0
jsmind/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "jsmind",
+  "version": "0.4.6",
+  "description": "jsMind is a pure javascript library for mindmap, it base on html5 canvas. jsMind was released under BSD li    cense, you can embed it in any project, if only you observe the license.",
+  "main": "js/jsmind.js",
+  "directories": {
+    "doc": "docs",
+    "example": "example"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/hizzgdev/jsmind.git"
+  },
+  "author": {
+    "name": "hizzgdev@163.com"
+  },
+  "license": "BSD-3-Clause",
+  "bugs": {
+    "url": "https://github.com/hizzgdev/jsmind/issues"
+  },
+  "homepage": "https://github.com/hizzgdev/jsmind#readme",
+  "keywords": [
+    "mindmap"
+  ],
+  "maintainers": [
+    {
+      "name": "hizzgdev",
+      "email": "hizzgdev@163.com"
+    }
+  ]
+}

BIN
jsmind/screenshots/jsmind.png


+ 160 - 0
jsmind/style/jsmind.css

@@ -0,0 +1,160 @@
+/*
+ * Released under BSD License
+ * Copyright (c) 2014-2021 hizzgdev@163.com
+ * 
+ * Project Home:
+ *   https://github.com/hizzgdev/jsmind/
+ */
+
+/* important section */
+.jsmind-inner{position:relative;overflow:auto;width:100%;height:100%;outline:none;}/*box-shadow:0 0 2px #000;*/
+.jsmind-inner{
+    moz-user-select:-moz-none;
+    -moz-user-select:none;
+    -o-user-select:none;
+    -khtml-user-select:none;
+    -webkit-user-select:none;
+    -ms-user-select:none;
+    user-select:none;
+}
+
+/* z-index:1 */
+svg.jsmind{position:absolute;z-index:1;}
+canvas.jsmind{position:absolute;z-index:1;}
+
+/* z-index:2 */
+jmnodes{position:absolute;z-index:2;background-color:rgba(0,0,0,0);}/*background color is necessary*/
+jmnode{position:absolute;cursor:default;max-width:400px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
+jmexpander{position:absolute;width:11px;height:11px;display:block;overflow:hidden;line-height:12px;font-size:12px;text-align:center;border-radius:6px;border-width:1px;border-style:solid;cursor:pointer;}
+
+/* default theme */
+jmnode{padding:10px;background-color:#fff;color:#333;border-radius:5px;box-shadow:1px 1px 1px #666;font:16px/1.125 Verdana,Arial,Helvetica,sans-serif;}
+jmnode:hover{box-shadow:2px 2px 8px #000;background-color:#ebebeb;color:#333;}
+jmnode.selected{background-color:#11f;color:#fff;box-shadow:2px 2px 8px #000;}
+jmnode.root{font-size:24px;}
+jmexpander{border-color:gray;}
+jmexpander:hover{border-color:#000;}
+
+@media screen and (max-device-width: 1024px) {
+    jmnode{padding:5px;border-radius:3px;font-size:14px;}
+    jmnode.root{font-size:21px;}
+}
+/* primary theme */
+jmnodes.theme-primary jmnode{background-color:#428bca;color:#fff;border-color:#357ebd;}
+jmnodes.theme-primary jmnode:hover{background-color:#3276b1;border-color:#285e8e;}
+jmnodes.theme-primary jmnode.selected{background-color:#f1c40f;color:#fff;}
+jmnodes.theme-primary jmnode.root{}
+jmnodes.theme-primary jmexpander{}
+jmnodes.theme-primary jmexpander:hover{}
+
+/* warning theme */
+jmnodes.theme-warning jmnode{background-color:#f0ad4e;border-color:#eea236;color:#fff;}
+jmnodes.theme-warning jmnode:hover{background-color:#ed9c28;border-color:#d58512;}
+jmnodes.theme-warning jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-warning jmnode.root{}
+jmnodes.theme-warning jmexpander{}
+jmnodes.theme-warning jmexpander:hover{}
+
+/* danger theme */
+jmnodes.theme-danger jmnode{background-color:#d9534f;border-color:#d43f3a;color:#fff;}
+jmnodes.theme-danger jmnode:hover{background-color:#d2322d;border-color:#ac2925;}
+jmnodes.theme-danger jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-danger jmnode.root{}
+jmnodes.theme-danger jmexpander{}
+jmnodes.theme-danger jmexpander:hover{}
+
+/* success theme */
+jmnodes.theme-success jmnode{background-color:#5cb85c;border-color:#4cae4c;color:#fff;}
+jmnodes.theme-success jmnode:hover{background-color:#47a447;border-color:#398439;}
+jmnodes.theme-success jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-success jmnode.root{}
+jmnodes.theme-success jmexpander{}
+jmnodes.theme-success jmexpander:hover{}
+
+/* info theme */
+jmnodes.theme-info jmnode{background-color:#5dc0de;border-color:#46b8da;;color:#fff;}
+jmnodes.theme-info jmnode:hover{background-color:#39b3d7;border-color:#269abc;}
+jmnodes.theme-info jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-info jmnode.root{}
+jmnodes.theme-info jmexpander{}
+jmnodes.theme-info jmexpander:hover{}
+
+/* greensea theme */
+jmnodes.theme-greensea jmnode{background-color:#1abc9c;color:#fff;}
+jmnodes.theme-greensea jmnode:hover{background-color:#16a085;}
+jmnodes.theme-greensea jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-greensea jmnode.root{}
+jmnodes.theme-greensea jmexpander{}
+jmnodes.theme-greensea jmexpander:hover{}
+
+/* nephrite theme */
+jmnodes.theme-nephrite jmnode{background-color:#2ecc71;color:#fff;}
+jmnodes.theme-nephrite jmnode:hover{background-color:#27ae60;}
+jmnodes.theme-nephrite jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-nephrite jmnode.root{}
+jmnodes.theme-nephrite jmexpander{}
+jmnodes.theme-nephrite jmexpander:hover{}
+
+/* belizehole theme */
+jmnodes.theme-belizehole jmnode{background-color:#3498db;color:#fff;}
+jmnodes.theme-belizehole jmnode:hover{background-color:#2980b9;}
+jmnodes.theme-belizehole jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-belizehole jmnode.root{}
+jmnodes.theme-belizehole jmexpander{}
+jmnodes.theme-belizehole jmexpander:hover{}
+
+/* wisteria theme */
+jmnodes.theme-wisteria jmnode{background-color:#9b59b6;color:#fff;}
+jmnodes.theme-wisteria jmnode:hover{background-color:#8e44ad;}
+jmnodes.theme-wisteria jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-wisteria jmnode.root{}
+jmnodes.theme-wisteria jmexpander{}
+jmnodes.theme-wisteria jmexpander:hover{}
+
+/* asphalt theme */
+jmnodes.theme-asphalt jmnode{background-color:#34495e;color:#fff;}
+jmnodes.theme-asphalt jmnode:hover{background-color:#2c3e50;}
+jmnodes.theme-asphalt jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-asphalt jmnode.root{}
+jmnodes.theme-asphalt jmexpander{}
+jmnodes.theme-asphalt jmexpander:hover{}
+
+/* orange theme */
+jmnodes.theme-orange jmnode{background-color:#f1c40f;color:#fff;}
+jmnodes.theme-orange jmnode:hover{background-color:#f39c12;}
+jmnodes.theme-orange jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-orange jmnode.root{}
+jmnodes.theme-orange jmexpander{}
+jmnodes.theme-orange jmexpander:hover{}
+
+/* pumpkin theme */
+jmnodes.theme-pumpkin jmnode{background-color:#e67e22;color:#fff;}
+jmnodes.theme-pumpkin jmnode:hover{background-color:#d35400;}
+jmnodes.theme-pumpkin jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-pumpkin jmnode.root{}
+jmnodes.theme-pumpkin jmexpander{}
+jmnodes.theme-pumpkin jmexpander:hover{}
+
+/* pomegranate theme */
+jmnodes.theme-pomegranate jmnode{background-color:#e74c3c;color:#fff;}
+jmnodes.theme-pomegranate jmnode:hover{background-color:#c0392b;}
+jmnodes.theme-pomegranate jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-pomegranate jmnode.root{}
+jmnodes.theme-pomegranate jmexpander{}
+jmnodes.theme-pomegranate jmexpander:hover{}
+
+/* clouds theme */
+jmnodes.theme-clouds jmnode{background-color:#ecf0f1;color:#333;}
+jmnodes.theme-clouds jmnode:hover{background-color:#bdc3c7;}
+jmnodes.theme-clouds jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-clouds jmnode.root{}
+jmnodes.theme-clouds jmexpander{}
+jmnodes.theme-clouds jmexpander:hover{}
+
+/* asbestos theme */
+jmnodes.theme-asbestos jmnode{background-color:#95a5a6;color:#fff;}
+jmnodes.theme-asbestos jmnode:hover{background-color:#7f8c8d;}
+jmnodes.theme-asbestos jmnode.selected{background-color:#11f;color:#fff;}
+jmnodes.theme-asbestos jmnode.root{}
+jmnodes.theme-asbestos jmexpander{}
+jmnodes.theme-asbestos jmexpander:hover{}