jack пре 2 година
родитељ
комит
e5d748d914
100 измењених фајлова са 21766 додато и 1145 уклоњено
  1. 5 3
      kityminder-editor/.gitignore
  2. 0 0
      kityminder-editor/.gitmodules
  3. 18 0
      kityminder-editor/.jsbeautifyrc
  4. 0 1
      kityminder-editor/.jshintrc
  5. 6 0
      kityminder-editor/CHANGELOG.md
  6. 48 135
      kityminder-editor/Gruntfile.js
  7. 27 340
      kityminder-editor/LICENSE
  8. 45 64
      kityminder-editor/README.md
  9. 71 0
      kityminder-editor/dev.html
  10. 80 0
      kityminder-editor/dist/kityminder.core.css
  11. 9358 0
      kityminder-editor/dist/kityminder.core.js
  12. 9 0
      kityminder-editor/dist/kityminder.core.min.js
  13. 316 0
      kityminder-editor/doc/Architecture.md
  14. 65 0
      kityminder-editor/example.html
  15. BIN
      kityminder-editor/icon.ico
  16. 101 0
      kityminder-editor/import.js
  17. 828 1
      kityminder-editor/node_modules/.package_versions.json
  18. 139 565
      kityminder-editor/package-lock.json
  19. 38 36
      kityminder-editor/package.json
  20. 49 0
      kityminder-editor/src/connect/arc.js
  21. 85 0
      kityminder-editor/src/connect/arc_tp.js
  22. 42 0
      kityminder-editor/src/connect/bezier.js
  23. 33 0
      kityminder-editor/src/connect/fish-bone-master.js
  24. 34 0
      kityminder-editor/src/connect/l.js
  25. 63 0
      kityminder-editor/src/connect/poly.js
  26. 50 0
      kityminder-editor/src/connect/under.js
  27. 34 0
      kityminder-editor/src/core/_boxv.js
  28. 43 0
      kityminder-editor/src/core/animate.js
  29. 169 0
      kityminder-editor/src/core/command.js
  30. 92 0
      kityminder-editor/src/core/compatibility.js
  31. 126 0
      kityminder-editor/src/core/connect.js
  32. 362 0
      kityminder-editor/src/core/data.js
  33. 269 0
      kityminder-editor/src/core/event.js
  34. 41 0
      kityminder-editor/src/core/focus.js
  35. 128 0
      kityminder-editor/src/core/keymap.js
  36. 66 0
      kityminder-editor/src/core/keyreceiver.js
  37. 11 0
      kityminder-editor/src/core/kity.js
  38. 523 0
      kityminder-editor/src/core/layout.js
  39. 40 0
      kityminder-editor/src/core/minder.js
  40. 151 0
      kityminder-editor/src/core/module.js
  41. 407 0
      kityminder-editor/src/core/node.js
  42. 34 0
      kityminder-editor/src/core/option.js
  43. 76 0
      kityminder-editor/src/core/paper.js
  44. 110 0
      kityminder-editor/src/core/patch.js
  45. 214 0
      kityminder-editor/src/core/promise.js
  46. 63 0
      kityminder-editor/src/core/readonly.js
  47. 261 0
      kityminder-editor/src/core/render.js
  48. 146 0
      kityminder-editor/src/core/select.js
  49. 154 0
      kityminder-editor/src/core/shortcut.js
  50. 60 0
      kityminder-editor/src/core/status.js
  51. 92 0
      kityminder-editor/src/core/template.js
  52. 175 0
      kityminder-editor/src/core/theme.js
  53. 65 0
      kityminder-editor/src/core/utils.js
  54. 3 0
      kityminder-editor/src/expose-kityminder.js
  55. 80 0
      kityminder-editor/src/kityminder.css
  56. 106 0
      kityminder-editor/src/kityminder.js
  57. 143 0
      kityminder-editor/src/layout/btree.js
  58. 89 0
      kityminder-editor/src/layout/filetree.js
  59. 68 0
      kityminder-editor/src/layout/fish-bone-master.js
  60. 72 0
      kityminder-editor/src/layout/fish-bone-slave.js
  61. 62 0
      kityminder-editor/src/layout/mind.js
  62. 76 0
      kityminder-editor/src/layout/tianpan.js
  63. 156 0
      kityminder-editor/src/module/arrange.js
  64. 126 0
      kityminder-editor/src/module/basestyle.js
  65. 172 0
      kityminder-editor/src/module/clipboard.js
  66. 405 0
      kityminder-editor/src/module/dragtree.js
  67. 293 0
      kityminder-editor/src/module/expand.js
  68. 159 0
      kityminder-editor/src/module/font.js
  69. 127 0
      kityminder-editor/src/module/hyperlink.js
  70. 111 0
      kityminder-editor/src/module/image-viewer.js
  71. 147 0
      kityminder-editor/src/module/image.js
  72. 171 0
      kityminder-editor/src/module/keynav.js
  73. 92 0
      kityminder-editor/src/module/layout.js
  74. 150 0
      kityminder-editor/src/module/node.js
  75. 116 0
      kityminder-editor/src/module/note.js
  76. 165 0
      kityminder-editor/src/module/outline.js
  77. 156 0
      kityminder-editor/src/module/priority.js
  78. 160 0
      kityminder-editor/src/module/progress.js
  79. 314 0
      kityminder-editor/src/module/resource.js
  80. 184 0
      kityminder-editor/src/module/select.js
  81. 114 0
      kityminder-editor/src/module/style.js
  82. 288 0
      kityminder-editor/src/module/text.js
  83. 397 0
      kityminder-editor/src/module/view.js
  84. 195 0
      kityminder-editor/src/module/zoom.js
  85. 18 0
      kityminder-editor/src/protocol/json.js
  86. 158 0
      kityminder-editor/src/protocol/markdown.js
  87. 273 0
      kityminder-editor/src/protocol/png.js
  88. 243 0
      kityminder-editor/src/protocol/svg.js
  89. 237 0
      kityminder-editor/src/protocol/text.js
  90. 38 0
      kityminder-editor/src/template/default.js
  91. 28 0
      kityminder-editor/src/template/filetree.js
  92. 41 0
      kityminder-editor/src/template/fish-bone.js
  93. 23 0
      kityminder-editor/src/template/right.js
  94. 22 0
      kityminder-editor/src/template/structure.js
  95. 29 0
      kityminder-editor/src/template/tianpan.js
  96. 66 0
      kityminder-editor/src/theme/default.js
  97. 57 0
      kityminder-editor/src/theme/fish.js
  98. 78 0
      kityminder-editor/src/theme/fresh.js
  99. 62 0
      kityminder-editor/src/theme/snow.js
  100. 74 0
      kityminder-editor/src/theme/tianpan.js

+ 5 - 3
kityminder-editor/.gitignore

@@ -1,5 +1,7 @@
 .idea
 .DS_Store
-ui/templates.js
-.tmp/
-upload/
+*.sublime-project
+*.sublime-workspace
+node_modules/
+_drafts/
+bower_components

+ 0 - 0
kityminder-editor/.gitmodules


+ 18 - 0
kityminder-editor/.jsbeautifyrc

@@ -0,0 +1,18 @@
+{
+    "indent_size": 4,
+    "indent_char": " ",
+    "indent_level": 0,
+    "indent_with_tabs": false,
+    "preserve_newlines": true,
+    "max_preserve_newlines": 10,
+    "jslint_happy": false,
+    "space_in_paren": false,
+    "brace_style": "collapse",
+    "keep_array_indentation": false,
+    "keep_function_indentation": false,
+    "space_before_conditional": false,
+    "break_chained_methods": false,
+    "eval_code": false,
+    "unescape_strings": false,
+    "wrap_line_length": 0
+}

+ 0 - 1
kityminder-editor/.jshintrc

@@ -9,7 +9,6 @@
     "quotmark": false,
     "browser": true,
     "boss": true,
-    "indent": 4,
     "predef" : [
         "define"
     ]

+ 6 - 0
kityminder-editor/CHANGELOG.md

@@ -0,0 +1,6 @@
+
+# KityMinder-Core 更新日志
+
+## v1.4.0
+
+完成从[原仓库](https://github.com/fex-team/kityminder)的代码迁移。处于兼容性考虑,版本号直接从 1.4.0 开始。

+ 48 - 135
kityminder-editor/Gruntfile.js

@@ -5,18 +5,19 @@ var path = require('path');
 module.exports = function(grunt) {
     'use strict';
 
-	// Load grunt tasks automatically
-	require('load-grunt-tasks')(grunt);
+    // These plugins provide necessary tasks.
+    /* [Build plugin & task ] ------------------------------------*/
+    grunt.loadNpmTasks('grunt-module-dependence');
+    grunt.loadNpmTasks('grunt-replace');
+    grunt.loadNpmTasks('grunt-contrib-clean');
+    grunt.loadNpmTasks('grunt-contrib-copy');
+    grunt.loadNpmTasks('grunt-contrib-concat');
+    grunt.loadNpmTasks('grunt-contrib-uglify');
     grunt.loadNpmTasks('grunt-browser-sync');
     grunt.loadNpmTasks('grunt-contrib-watch');
 
     var pkg = grunt.file.readJSON('package.json');
 
-	var appConfig = {
-		app: require('./bower.json').appPath || 'app',
-		dist: 'dist'
-	};
-
     var banner = '/*!\n' +
         ' * ====================================================\n' +
         ' * <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
@@ -28,7 +29,7 @@ module.exports = function(grunt) {
         ' * ====================================================\n' +
         ' */\n\n';
 
-    var expose = '\nuse(\'expose-editor\');\n';
+    var expose = '\nuse(\'expose-kityminder\');\n';
 
     // Project configuration.
     grunt.initConfig({
@@ -36,36 +37,17 @@ module.exports = function(grunt) {
         // Metadata.
         pkg: pkg,
 
-	    yeoman: appConfig,
-
         clean: {
-            last: [
-	            '.tmp',
-	            'dist/*.js',
-	            'dist/*.css',
-	            'dist/*.css.map'
-            ],
-	        clstmp: ['.tmp']
+            last: 'dist'
         },
 
-        // resolve dependence
-        dependence: {
-            options: {
-                base: 'src',
-                entrance: 'expose-editor'
-            },
-            merge: {
-                files: [{
-                    src: [
-                        'src/**/*.js'
-                    ],
-                    dest: '.tmp/scripts/kityminder.editor.logic.js'
-                }]
-            }
+        // watch
+        watch: {
+            files: 'src/**',
+            tasks: ['build']
         },
-
         // browser sync for dev
-		browserSync: {
+        browserSync: {
             bsFiles: {
                 dist: 'dist/css/*.css',
                 src: 'src/**'
@@ -73,29 +55,35 @@ module.exports = function(grunt) {
             options: {
                 server: {
                     baseDir: './',
-                    index: 'index.html',
+                    index: 'dev.html',
                     watchTask: true
                 }
             }
-		},
+        },
+
+        // resolve dependence
+        dependence: {
+            options: {
+                base: 'src',
+                entrance: 'expose-kityminder'
+            },
+            merge: {
+                files: [{
+                    src: 'src/**/*.js',
+                    dest: 'dist/kityminder.core.js'
+                }]
+            }
+        },
 
-        // concat
+        // concat, just add closure
         concat: {
-            closure: {
-                options: {
-                    banner: banner + '(function () {\n',
-                    footer: expose + '})();'
-                },
+            options: {
+                banner: banner + '(function () {\n',
+                footer: expose + '})();'
+            },
+            build: {
                 files: {
-	                'dist/kityminder.editor.js': [
-		                '.tmp/scripts/kityminder.editor.logic.js',
-		                '.tmp/scripts/kityminder.app.annotated.js',
-		                '.tmp/scripts/templates.annotated.js',
-		                '.tmp/scripts/service/*.js',
-		                '.tmp/scripts/filter/*.js',
-                        '.tmp/scripts/dialog/**/*.js',
-		                '.tmp/scripts/directive/**/*.js'
-	                ]
+                    'dist/kityminder.core.js': ['dist/kityminder.core.js']
                 }
             }
         },
@@ -105,98 +93,23 @@ module.exports = function(grunt) {
                 banner: banner
             },
             minimize: {
-                files: [{
-	                src: 'dist/kityminder.editor.js',
-	                dest: 'dist/kityminder.editor.min.js'
-                }]
+                src: 'dist/kityminder.core.js',
+                dest: 'dist/kityminder.core.min.js'
             }
         },
 
-        less: {
-            compile: {
-                options: {
-                    sourceMap: true,
-	                sourceMapURL: 'kityminder.editor.css.map',
-                    sourceMapFilename: 'dist/kityminder.editor.css.map'
-                },
-                files: [{
-                    dest: 'dist/kityminder.editor.css',
-                    src: 'less/editor.less'
-                }]
+        copy: {
+            dist: {
+                src: 'src/kityminder.css',
+                dest: "dist/kityminder.core.css"
             }
-        },
-
-	    cssmin: {
-	        dist: {
-	            files: {
-	                'dist/kityminder.editor.min.css': 'dist/kityminder.editor.css'
-	         }
-	       }
-	    },
-
-	    ngtemplates: {
-		    kityminderEditor: {
-			    src: ['ui/directive/**/*.html', 'ui/dialog/**/*.html'],
-			    dest: 'ui/templates.js',
-			    options: {
-				    htmlmin: {
-					    collapseBooleanAttributes: true,
-					    collapseWhitespace: true,
-					    removeComments: true
-				    }
-			    }
-		    }
-	    },
-
-	    // Automatically inject Bower components into the app
-	    wiredep: {
-		    dev: {
-			    src: ['index.html'],
-			    devDependencies: true
-		    },
-		    dist: {
-			    src: ['dist/index.html']
-		    }
-	    },
-
-	    // Copies remaining files to places other tasks can use
-	    copy: {
-		    dist: {
-				files: [{
-				    expand: true,
-				    cwd: 'ui',
-					src: 'images/*',
-				    dest: 'dist'
-
-			    }]
-		    }
-	    },
-
-
-	    // ng-annotate tries to make the code safe for minification automatically
-	    // by using the Angular long form for dependency injection.
-	    ngAnnotate: {
-		    dist: {
-			    files: [{
-				    expand: true,
-				    cwd: 'ui/',
-				    src: '**/*.js',
-				    ext: '.annotated.js',
-				    extDot: 'last',
-				    dest: '.tmp/scripts/'
-			    }]
-		    }
-	    }
-
+        }
 
     });
 
+
     // Build task(s).
-	grunt.registerTask('build', ['clean:last',
-		//'wiredep:dist',
-        'ngtemplates', 'dependence', 'ngAnnotate', 'concat', 'uglify', 'less', 'cssmin', 'copy', 'clean:clstmp']);
+    grunt.registerTask('build', ['clean', 'dependence', 'concat:build', 'uglify:minimize', 'copy']);
+    grunt.registerTask('dev', ['browserSync', 'watch']);
 
-	grunt.registerTask('dev', ['clean:last',
-        //'wiredep:dev',
-        'ngtemplates', 'dependence', 'ngAnnotate', 'concat', 'uglify', 'less', 'cssmin', 'copy', 'clean:clstmp', 'browserSync', 'watch']);
 };

+ 27 - 340
kityminder-editor/LICENSE

@@ -1,340 +1,27 @@
-GNU GENERAL PUBLIC LICENSE
-                       Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.)  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
-  1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
-
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
-
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
-
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-                            NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    {description}
-    Copyright (C) {year}  {fullname}
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  {signature of Ty Coon}, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
-
+Copyright (c) 2014, FEX, Baidu.
+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 KityMinder 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.

+ 45 - 64
kityminder-editor/README.md

@@ -1,87 +1,68 @@
-KityMinder Editor
+KityMinder Core
 ==========
 
 ## 简介
 
-KityMinder Editor 是一款强大、简洁、体验优秀的脑图编辑工具,适合用于编辑树/图/网等结构的数据
+KityMinder 是一款强大的脑图可视化/编辑工具,由百度 FEX 团队开发并维护
 
-编辑器由百度 [FEX](https://github.com/fex-team) 基于 [kityminder-core](https://github.com/fex-team/kityminder-core) 搭建,并且在[百度脑图](http://naotu.baidu.com)中使用。
+本仓库是 KityMinder 的核心实现部分:
 
-他们的区别与联系如下:
+* 包括脑图数据的可视化展示(Json 格式)
+* 包括简单的编辑功能(节点创建、编辑、删除)。更加强大编辑功能的 KityMinder 编辑器请移步 [kityminder-editor](https://github.com/fex-team/kityminder-editor)
+* 不包含第三方格式(FreeMind、XMind、MindManager)的支持,可以加载 [kityminder-protocol](https://github.com/fex-team/kityminder-third-party-protocol) 来扩展第三方格式支持。
+* 不包含文件存储的支持,需要自行实现存储。可参照[百度脑图](https://naotu.baidu.com)中的开源的 fio + 百度网盘方案进行实现。
 
-![KityMinder 联系](relations.png "KityMinder 联系")
+## 使用
 
-- [kityminder-core](https://github.com/fex-team/kityminder-core) 是 kityminder 的核心部分,基于百度 [FEX](https://github.com/fex-team) 开发的矢量图形库 [kity](https://github.com/fex-team/kity)。包含了脑图数据的可视化展现,简单编辑功能等所有底层支持。
-- [kityminder-editor](https://github.com/fex-team/kityminder-editor) 基于 kityminder-core 搭建,依赖于 AngularJS,包含 UI 和热盒 [hotbox](https://github.com/fex-team/hotbox) 等方便用户输入的功能,简单来说,就是一款编辑器。
-- [百度脑图](http://naotu.baidu.com) 基于 kityminder-editor,加入了第三方格式导入导出 (FreeMind, XMind, MindManager) 、文件储存、用户认证、文件分享、历史版本等业务逻辑。
+可以参考 [example.html](example.html) 进行使用。
 
-## 功能
-
-- 基本操作:文本编辑,节点折叠、插入、删除、排序、归纳、复制、剪切、粘贴等
-- 样式控制:字体、加粗、斜体、颜色、样式拷贝、样式粘贴等
-- 图标:优先级、进度等
-- 历史:撤销/重做
-- 标签:多标签贴入
-- 备注:支持 Markdown 格式备注
-- 图片:支持本地/网络/搜索图片插入
-- 超链接:支持 HTTP/HTTPS/MAIL/FTP 链接插入
-- 布局:支持多种布局切换
-- 主题:支持多种主题切换
-- 数据导入导出:支持多种格式的导入,多种格式(包括图片)的导出
-- 缩略图:支持缩略图查看/导航
+```js
+<div id="minder-container"></div>
+<script type="text/javascript" src="kityminder.core.min.js"></script>
+<script type="text/javascript">
+var minder = new kityminder.Minder({
+	renderTo: '#minder-container'
+});
+</script>
+```
 
-## 开发使用
-根目录下的 `index.html` 为开发环境,`dist` 目录下的 `index.html` 使用打包好的代码,适用于线上环境。
+更多详细的开发资料可以参考 [wiki](https://github.com/fex-team/kityminder-core/wiki)
 
-1. 安装 [nodejs](http://nodejs.org) 和 [npm](https://docs.npmjs.com/getting-started/installing-node)
-2. 初始化:切到 kityminder-editor 根目录下运行 `npm run init`
-3. 在 kityminder-editor 根目录下运行 `grunt dev` 即可启动项目
-4. 你可以基于根目录的 `index.html` 开发,或者查看 `dist` 目录下用于生产环境的 `index.html`,Enjoy it!
+## 兼容性
 
-另外,kityminder-editor 还提供了 bower 包,方便开发者直接使用。你可以在需要用到 kityminder-editor 的工程目录下
-运行 `bower install kityminder-editor`,接着手动引入 kityminder-editor 所依赖的 css 和 js 文件,具体文件见
-`dist` 目录下的 `index.html`,推荐使用 npm 包 [wireDep](https://www.npmjs.com/package/wiredep) 自动进行,
-可参考根目录下 `Gruntfile.js`。
+KityMinder 基于 SVG 技术实现,支持绝大多数的 HTML5 浏览器,包括:
 
-## 构建
-运行 `grunt build`,完成后 `dist` 目录里就是可用运行的 kityminder-editor, 双击 `index.html` 即可打开运行示例
+1. Chrome
+2. Firefox
+3. Safari
+4. Internet Explorer 10 或以上
 
-## 初始化配置
-用户可以根据需要,配置 `kityminder-editor`, 具体使用方法如下:
-```
-angular.module('kityminderDemo', ['kityminderEditor'])
-    .config(function (configProvider) {
-        configProvider.set('imageUpload', 'path/to/image/upload/handler');
-    });
-
-```
+## 使用说明
 
-## 数据导入导出
-由于 kityminder-editor 是基于 kityminder-core 搭建的,而 kityminder-core 内置了五种常见
-格式的导入或导出,在创建编辑器实例之后,可以使用四个接口进行数据的导入导出。
+kityminder-core 依赖于 [kity](https://github.com/fex-team/kity),开发中用到 seajs 进行异步加载。
+例子中 dev.html 使用 seajs 进行包加载,example.html 使用同步加载的方式。
+使用步骤如下:
 
-* `editor.minder.exportJson()` - 导出脑图数据为 JSON 对象
-* `editor.minder.importJson(json)` - 导入 JSON 对象为当前脑图数据
-* `editor.minder.exportData(protocol, option)` - 导出脑图数据为指定的数据格式,返回一个 Promise,其值为导出的结果
-* `editor.minder.importData(protocol, data, option)` - 导入指定格式的数据为脑图数据,返回一个 Promise,其值为转换之后的脑图 Json 数据
+1. 安装 [bower](http://bower.io/#install-bower)
+2. 切换到 kityminder-core 目录下,运行:
 
-目前支持的数据格式包括:
+```bash
+bower install
+```
 
-* `json` - JSON 字符串,支持导入和导出
-* `text` - 纯文本格式,支持导入和导出
-* `markdown` - Markdown 格式,支持导入和导出
-* `svg` - SVG 矢量格式,仅支持导出
-* `png` - PNG 位图格式,仅支持导出
+## 开发说明
 
-更多格式的支持,可以加载 [kityminder-protocol](https://github.com/fex-team/kityminder-protocol) 来扩展第三方格式支持。
+1. 安装 [bower](http://bower.io/#install-bower)
+2. 安装 [npm](https://www.npmjs.com/get-npm)
 
-数据格式的具体信息,可参考 [kityminder-core-wiki 的中的说明](https://github.com/fex-team/kityminder-core/wiki)。
+```bash
+bower install
+npm install
+npm run dev
+```
 
 ## 联系我们
-问题和建议反馈:
-
-[Github issues](https://github.com/fex-team/kityminder-editor/issues)
-
-邮件组:kity@baidu.com
 
-QQ 讨论群:475962105
+问题和建议反馈:[Github Issues](https://github.com/fex-team/kityminder-core/issues)
+邮件组: kity@baidu.com
+QQ 讨论群: 374918234

+ 71 - 0
kityminder-editor/dev.html

@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>KityMinder Example</title>
+
+    <link href="favicon.ico" type="image/x-icon" rel="shortcut icon">
+    <link rel="stylesheet" href="src/kityminder.css" rel="stylesheet">
+
+    <style type="text/css">
+        body {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+        }
+
+        #minder-view {
+            position: absolute;
+            border: 1px solid #ccc;
+            left: 10px;
+            top: 10px;
+            bottom: 10px;
+            right: 10px;
+        }
+    </style>
+
+    <script type="text/javascript" src="node_modules/kity/dist/kity.js"></script>
+</head>
+<body>
+    <script id="minder-view" type="application/kityminder" minder-data-type="json">
+    {
+        "root": {
+            "data": {
+                "text": "百度产品",
+                "image": "https://www.baidu.com/img/bd_logo1.png?where=super",
+                "imageSize": { "width": 270, "height": 129 }
+            },
+            "children": [
+                { "data": { "text": "新闻" } },
+                { "data": { "text": "网页", "priority": 1 } },
+                { "data": { "text": "贴吧", "priority": 2 } },
+                { "data": { "text": "知道", "priority": 2 } },
+                { "data": { "text": "音乐", "priority": 3 } },
+                { "data": { "text": "图片", "priority": 3 } },
+                { "data": { "text": "视频", "priority": 3 } },
+                { "data": { "text": "地图", "priority": 3 } },
+                { "data": { "text": "百科", "priority": 3 } },
+                { "data": { "text": "更多", "hyperlink": "http://www.baidu.com/more" } }
+            ]
+        }
+    }
+    </script>
+</body>
+
+<!-- *************************** Module 形式加载引入 **************************** -->
+<script type="text/javascript" src="node_modules/seajs/dist/sea-debug.js"></script>
+<script type="text/javascript">
+    /* global seajs */
+    seajs.config({
+        base: './src'
+    });
+    define('start', function(require) {
+        var Minder = require('kityminder').Minder;
+
+        // 创建 km 实例
+        var km = window.km = new Minder();
+        km.setup('#minder-view');
+    });
+    seajs.use('start');
+</script>
+</html>

Разлика између датотеке није приказан због своје велике величине
+ 80 - 0
kityminder-editor/dist/kityminder.core.css


+ 9358 - 0
kityminder-editor/dist/kityminder.core.js

@@ -0,0 +1,9358 @@
+/*!
+ * ====================================================
+ * Kity Minder Core - v1.4.50 - 2018-09-17
+ * https://github.com/fex-team/kityminder-core
+ * GitHub: https://github.com/fex-team/kityminder-core.git 
+ * Copyright (c) 2018 Baidu FEX; Licensed BSD-3-Clause
+ * ====================================================
+ */
+
+(function () {
+var _p = {
+    r: function(index) {
+        if (_p[index].inited) {
+            return _p[index].value;
+        }
+        if (typeof _p[index].value === "function") {
+            var module = {
+                exports: {}
+            }, returnValue = _p[index].value(null, module.exports, module);
+            _p[index].inited = true;
+            _p[index].value = returnValue;
+            if (returnValue !== undefined) {
+                return returnValue;
+            } else {
+                for (var key in module.exports) {
+                    if (module.exports.hasOwnProperty(key)) {
+                        _p[index].inited = true;
+                        _p[index].value = module.exports;
+                        return module.exports;
+                    }
+                }
+            }
+        } else {
+            _p[index].inited = true;
+            return _p[index].value;
+        }
+    }
+};
+
+//src/connect/arc.js
+/**
+ * @fileOverview
+ *
+ * 圆弧连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[0] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        var connectMarker = new kity.Marker().pipe(function() {
+            var r = 7;
+            var dot = new kity.Circle(r - 1);
+            this.addShape(dot);
+            this.setRef(r - 1, 0).setViewBox(-r, -r, r + r, r + r).setWidth(r).setHeight(r);
+            this.dot = dot;
+            this.node.setAttribute("markerUnits", "userSpaceOnUse");
+        });
+        connect.register("arc", function(node, parent, connection, width, color) {
+            var box = node.getLayoutBox(), pBox = parent.getLayoutBox();
+            var start, end, vector;
+            var abs = Math.abs;
+            var pathData = [];
+            var side = box.x > pBox.x ? "right" : "left";
+            node.getMinder().getPaper().addResource(connectMarker);
+            start = new kity.Point(pBox.cx, pBox.cy);
+            end = side == "left" ? new kity.Point(box.right + 2, box.cy) : new kity.Point(box.left - 2, box.cy);
+            vector = kity.Vector.fromPoints(start, end);
+            pathData.push("M", start);
+            pathData.push("A", abs(vector.x), abs(vector.y), 0, 0, vector.x * vector.y > 0 ? 0 : 1, end);
+            connection.setMarker(connectMarker);
+            connectMarker.dot.fill(color);
+            connection.setPathData(pathData);
+        });
+    }
+};
+
+//src/connect/arc_tp.js
+/**
+ *
+ * 圆弧连线
+ *
+ * @author: along
+ * @copyright: bpd729@163.com , 2015
+ */
+_p[1] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        var connectMarker = new kity.Marker().pipe(function() {
+            var r = 7;
+            var dot = new kity.Circle(r - 1);
+            this.addShape(dot);
+            this.setRef(r - 1, 0).setViewBox(-r, -r, r + r, r + r).setWidth(r).setHeight(r);
+            this.dot = dot;
+            this.node.setAttribute("markerUnits", "userSpaceOnUse");
+        });
+        /**
+     * 天盘图连线除了连接当前节点和前一个节点外, 还需要渲染当前节点和后一个节点的连接, 防止样式上的断线
+     * 这是天盘图与其余的模板不同的地方
+     */
+        connect.register("arc_tp", function(node, parent, connection, width, color) {
+            var end_box = node.getLayoutBox(), start_box = parent.getLayoutBox();
+            var index = node.getIndex();
+            var nextNode = parent.getChildren()[index + 1];
+            if (node.getIndex() > 0) {
+                start_box = parent.getChildren()[index - 1].getLayoutBox();
+            }
+            var start, end, vector;
+            var abs = Math.abs;
+            var pathData = [];
+            var side = end_box.x > start_box.x ? "right" : "left";
+            node.getMinder().getPaper().addResource(connectMarker);
+            start = new kity.Point(start_box.cx, start_box.cy);
+            end = new kity.Point(end_box.cx, end_box.cy);
+            var jl = Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2));
+            //两圆中心点距离
+            jl = node.getIndex() == 0 ? jl * .4 : jl;
+            vector = kity.Vector.fromPoints(start, end);
+            pathData.push("M", start);
+            pathData.push("A", jl, jl, 0, 0, 1, end);
+            connection.setMarker(connectMarker);
+            connectMarker.dot.fill(color);
+            connection.setPathData(pathData);
+            // 设置下一个的节点的连接线
+            if (nextNode && nextNode.getConnection()) {
+                var nextConnection = nextNode.getConnection();
+                var next_end_box = nextNode.getLayoutBox();
+                var next_end = new kity.Point(next_end_box.cx, next_end_box.cy);
+                var jl2 = Math.sqrt(Math.pow(end.x - next_end.x, 2) + Math.pow(end.y - next_end.y, 2));
+                //两圆中心点距离
+                pathData = [];
+                pathData.push("M", end);
+                pathData.push("A", jl2, jl2, 0, 0, 1, next_end);
+                nextConnection.setMarker(connectMarker);
+                connectMarker.dot.fill(color);
+                nextConnection.setPathData(pathData);
+            }
+        });
+    }
+};
+
+//src/connect/bezier.js
+/**
+ * @fileOverview
+ *
+ * 提供折线相连的方法
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[2] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        connect.register("bezier", function(node, parent, connection) {
+            // 连线起点和终点
+            var po = parent.getLayoutVertexOut(), pi = node.getLayoutVertexIn();
+            // 连线矢量和方向
+            var v = parent.getLayoutVectorOut().normalize();
+            var r = Math.round;
+            var abs = Math.abs;
+            var pathData = [];
+            pathData.push("M", r(po.x), r(po.y));
+            if (abs(v.x) > abs(v.y)) {
+                // x - direction
+                var hx = (pi.x + po.x) / 2;
+                pathData.push("C", hx, po.y, hx, pi.y, pi.x, pi.y);
+            } else {
+                // y - direction
+                var hy = (pi.y + po.y) / 2;
+                pathData.push("C", po.x, hy, pi.x, hy, pi.x, pi.y);
+            }
+            connection.setMarker(null);
+            connection.setPathData(pathData);
+        });
+    }
+};
+
+//src/connect/fish-bone-master.js
+/**
+ * @fileOverview
+ *
+ * 鱼骨头主干连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[3] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        connect.register("fish-bone-master", function(node, parent, connection) {
+            var pout = parent.getLayoutVertexOut(), pin = node.getLayoutVertexIn();
+            var abs = Math.abs;
+            var dy = abs(pout.y - pin.y), dx = abs(pout.x - pin.x);
+            var pathData = [];
+            pathData.push("M", pout.x, pout.y);
+            pathData.push("h", dx - dy);
+            pathData.push("L", pin.x, pin.y);
+            connection.setMarker(null);
+            connection.setPathData(pathData);
+        });
+    }
+};
+
+//src/connect/l.js
+/**
+ * @fileOverview
+ *
+ * "L" 连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[4] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        connect.register("l", function(node, parent, connection) {
+            var po = parent.getLayoutVertexOut();
+            var pi = node.getLayoutVertexIn();
+            var vo = parent.getLayoutVectorOut();
+            var pathData = [];
+            var r = Math.round, abs = Math.abs;
+            pathData.push("M", po.round());
+            if (abs(vo.x) > abs(vo.y)) {
+                pathData.push("H", r(pi.x));
+            } else {
+                pathData.push("V", pi.y);
+            }
+            pathData.push("L", pi);
+            connection.setPathData(pathData);
+        });
+    }
+};
+
+//src/connect/poly.js
+/**
+ * @fileOverview
+ *
+ * 提供折线相连的方法
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[5] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        connect.register("poly", function(node, parent, connection, width) {
+            // 连线起点和终点
+            var po = parent.getLayoutVertexOut(), pi = node.getLayoutVertexIn();
+            // 连线矢量和方向
+            var v = parent.getLayoutVectorOut().normalize();
+            var r = Math.round;
+            var abs = Math.abs;
+            var pathData = [];
+            pathData.push("M", r(po.x), r(po.y));
+            switch (true) {
+              case abs(v.x) > abs(v.y) && v.x < 0:
+                // left
+                pathData.push("h", -parent.getStyle("margin-left"));
+                pathData.push("v", pi.y - po.y);
+                pathData.push("H", pi.x);
+                break;
+
+              case abs(v.x) > abs(v.y) && v.x >= 0:
+                // right
+                pathData.push("h", parent.getStyle("margin-right"));
+                pathData.push("v", pi.y - po.y);
+                pathData.push("H", pi.x);
+                break;
+
+              case abs(v.x) <= abs(v.y) && v.y < 0:
+                // top
+                pathData.push("v", -parent.getStyle("margin-top"));
+                pathData.push("h", pi.x - po.x);
+                pathData.push("V", pi.y);
+                break;
+
+              case abs(v.x) <= abs(v.y) && v.y >= 0:
+                // bottom
+                pathData.push("v", parent.getStyle("margin-bottom"));
+                pathData.push("h", pi.x - po.x);
+                pathData.push("V", pi.y);
+                break;
+            }
+            connection.setMarker(null);
+            connection.setPathData(pathData);
+        });
+    }
+};
+
+//src/connect/under.js
+/**
+ * @fileOverview
+ *
+ * 下划线连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[6] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var connect = _p.r(11);
+        connect.register("under", function(node, parent, connection, width, color) {
+            var box = node.getLayoutBox(), pBox = parent.getLayoutBox();
+            var start, end, vector;
+            var abs = Math.abs;
+            var pathData = [];
+            var side = box.x > pBox.x ? "right" : "left";
+            var radius = node.getStyle("connect-radius");
+            var underY = box.bottom + 3;
+            var startY = parent.getType() == "sub" ? pBox.bottom + 3 : pBox.cy;
+            var p1, p2, p3, mx;
+            if (side == "right") {
+                p1 = new kity.Point(pBox.right, startY);
+                p2 = new kity.Point(box.left - 10, underY);
+                p3 = new kity.Point(box.right, underY);
+            } else {
+                p1 = new kity.Point(pBox.left, startY);
+                p2 = new kity.Point(box.right + 10, underY);
+                p3 = new kity.Point(box.left, underY);
+            }
+            mx = (p1.x + p2.x) / 2;
+            pathData.push("M", p1);
+            pathData.push("C", mx, p1.y, mx, p2.y, p2);
+            pathData.push("L", p3);
+            connection.setMarker(null);
+            connection.setPathData(pathData);
+        });
+    }
+};
+
+//src/core/_boxv.js
+/**
+ * @fileOverview
+ *
+ * 调试工具:为 kity.Box 提供一个可视化的渲染
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[7] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Minder = _p.r(19);
+        if (location.href.indexOf("boxv") != -1) {
+            var vrect;
+            Object.defineProperty(kity.Box.prototype, "visualization", {
+                get: function() {
+                    if (!vrect) return null;
+                    return vrect.setBox(this);
+                }
+            });
+            Minder.registerInitHook(function() {
+                this.on("paperrender", function() {
+                    vrect = new kity.Rect();
+                    vrect.fill("rgba(200, 200, 200, .5)");
+                    vrect.stroke("orange");
+                    this.getRenderContainer().addShape(vrect);
+                });
+            });
+        }
+    }
+};
+
+//src/core/animate.js
+/**
+ * @fileOverview
+ *
+ * 动画控制
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[8] = {
+    value: function(require, exports, module) {
+        var Minder = _p.r(19);
+        var animateDefaultOptions = {
+            enableAnimation: true,
+            layoutAnimationDuration: 300,
+            viewAnimationDuration: 100,
+            zoomAnimationDuration: 300
+        };
+        var resoredAnimationOptions = {};
+        Minder.registerInitHook(function() {
+            this.setDefaultOptions(animateDefaultOptions);
+            if (!this.getOption("enableAnimation")) {
+                this.disableAnimation();
+            }
+        });
+        Minder.prototype.enableAnimation = function() {
+            for (var name in animateDefaultOptions) {
+                if (animateDefaultOptions.hasOwnProperty(name)) {
+                    this.setOption(resoredAnimationOptions[name]);
+                }
+            }
+        };
+        Minder.prototype.disableAnimation = function() {
+            for (var name in animateDefaultOptions) {
+                if (animateDefaultOptions.hasOwnProperty(name)) {
+                    resoredAnimationOptions[name] = this.getOption(name);
+                    this.setOption(name, 0);
+                }
+            }
+        };
+    }
+};
+
+//src/core/command.js
+_p[9] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var MinderEvent = _p.r(13);
+        var COMMAND_STATE_NORMAL = 0;
+        var COMMAND_STATE_DISABLED = -1;
+        var COMMAND_STATE_ACTIVED = 1;
+        /**
+     * 表示一个命令,包含命令的查询及执行
+     */
+        var Command = kity.createClass("Command", {
+            constructor: function() {
+                this._isContentChange = true;
+                this._isSelectionChange = false;
+            },
+            execute: function(minder, args) {
+                throw new Error("Not Implement: Command.execute()");
+            },
+            setContentChanged: function(val) {
+                this._isContentChange = !!val;
+            },
+            isContentChanged: function() {
+                return this._isContentChange;
+            },
+            setSelectionChanged: function(val) {
+                this._isSelectionChange = !!val;
+            },
+            isSelectionChanged: function() {
+                return this._isContentChange;
+            },
+            queryState: function(km) {
+                return COMMAND_STATE_NORMAL;
+            },
+            queryValue: function(km) {
+                return 0;
+            },
+            isNeedUndo: function() {
+                return true;
+            }
+        });
+        Command.STATE_NORMAL = COMMAND_STATE_NORMAL;
+        Command.STATE_ACTIVE = COMMAND_STATE_ACTIVED;
+        Command.STATE_DISABLED = COMMAND_STATE_DISABLED;
+        kity.extendClass(Minder, {
+            _getCommand: function(name) {
+                return this._commands[name.toLowerCase()];
+            },
+            _queryCommand: function(name, type, args) {
+                var cmd = this._getCommand(name);
+                if (cmd) {
+                    var queryCmd = cmd["query" + type];
+                    if (queryCmd) return queryCmd.apply(cmd, [ this ].concat(args));
+                }
+                return 0;
+            },
+            /**
+         * @method queryCommandState()
+         * @for Minder
+         * @description 查询指定命令的状态
+         *
+         * @grammar queryCommandName(name) => {number}
+         *
+         * @param {string} name 要查询的命令名称
+         *
+         * @return {number}
+         *   -1: 命令不存在或命令当前不可用
+         *    0: 命令可用
+         *    1: 命令当前可用并且已经执行过
+         */
+            queryCommandState: function(name) {
+                return this._queryCommand(name, "State", [].slice.call(arguments, 1));
+            },
+            /**
+         * @method queryCommandValue()
+         * @for Minder
+         * @description 查询指定命令当前的执行值
+         *
+         * @grammar queryCommandValue(name) => {any}
+         *
+         * @param {string} name 要查询的命令名称
+         *
+         * @return {any}
+         *    如果命令不存在,返回 undefined
+         *    不同命令具有不同返回值,具体请查看 [Command](command) 章节
+         */
+            queryCommandValue: function(name) {
+                return this._queryCommand(name, "Value", [].slice.call(arguments, 1));
+            },
+            /**
+         * @method execCommand()
+         * @for Minder
+         * @description 执行指定的命令。
+         *
+         * @grammar execCommand(name, args...)
+         *
+         * @param {string} name 要执行的命令名称
+         * @param {argument} args 要传递给命令的其它参数
+         */
+            execCommand: function(name) {
+                if (!name) return null;
+                name = name.toLowerCase();
+                var cmdArgs = [].slice.call(arguments, 1), cmd, stoped, result, eventParams;
+                var me = this;
+                cmd = this._getCommand(name);
+                eventParams = {
+                    command: cmd,
+                    commandName: name.toLowerCase(),
+                    commandArgs: cmdArgs
+                };
+                if (!cmd || !~this.queryCommandState(name)) {
+                    return false;
+                }
+                if (!this._hasEnterExecCommand) {
+                    this._hasEnterExecCommand = true;
+                    stoped = this._fire(new MinderEvent("beforeExecCommand", eventParams, true));
+                    if (!stoped) {
+                        this._fire(new MinderEvent("preExecCommand", eventParams, false));
+                        result = cmd.execute.apply(cmd, [ me ].concat(cmdArgs));
+                        this._fire(new MinderEvent("execCommand", eventParams, false));
+                        if (cmd.isContentChanged()) {
+                            this._firePharse(new MinderEvent("contentchange"));
+                        }
+                        this._interactChange();
+                    }
+                    this._hasEnterExecCommand = false;
+                } else {
+                    result = cmd.execute.apply(cmd, [ me ].concat(cmdArgs));
+                    if (!this._hasEnterExecCommand) {
+                        this._interactChange();
+                    }
+                }
+                return result === undefined ? null : result;
+            }
+        });
+        module.exports = Command;
+    }
+};
+
+//src/core/compatibility.js
+_p[10] = {
+    value: function(require, exports, module) {
+        var utils = _p.r(33);
+        function compatibility(json) {
+            var version = json.version || (json.root ? "1.4.0" : "1.1.3");
+            switch (version) {
+              case "1.1.3":
+                c_113_120(json);
+
+              /* falls through */
+                case "1.2.0":
+              case "1.2.1":
+                c_120_130(json);
+
+              /* falls through */
+                case "1.3.0":
+              case "1.3.1":
+              case "1.3.2":
+              case "1.3.3":
+              case "1.3.4":
+              case "1.3.5":
+                /* falls through */
+                c_130_140(json);
+            }
+            return json;
+        }
+        function traverse(node, fn) {
+            fn(node);
+            if (node.children) node.children.forEach(function(child) {
+                traverse(child, fn);
+            });
+        }
+        /* 脑图数据升级 */
+        function c_120_130(json) {
+            traverse(json, function(node) {
+                var data = node.data;
+                delete data.layout_bottom_offset;
+                delete data.layout_default_offset;
+                delete data.layout_filetree_offset;
+            });
+        }
+        /**
+     * 脑图数据升级
+     * v1.1.3 => v1.2.0
+     * */
+        function c_113_120(json) {
+            // 原本的布局风格
+            var ocs = json.data.currentstyle;
+            delete json.data.currentstyle;
+            // 为 1.2 选择模板,同时保留老版本文件的皮肤
+            if (ocs == "bottom") {
+                json.template = "structure";
+                json.theme = "snow";
+            } else if (ocs == "default") {
+                json.template = "default";
+                json.theme = "classic";
+            }
+            traverse(json, function(node) {
+                var data = node.data;
+                // 升级优先级、进度图标
+                if ("PriorityIcon" in data) {
+                    data.priority = data.PriorityIcon;
+                    delete data.PriorityIcon;
+                }
+                if ("ProgressIcon" in data) {
+                    data.progress = 1 + (data.ProgressIcon - 1 << 1);
+                    delete data.ProgressIcon;
+                }
+                // 删除过时属性
+                delete data.point;
+                delete data.layout;
+            });
+        }
+        function c_130_140(json) {
+            json.root = {
+                data: json.data,
+                children: json.children
+            };
+            delete json.data;
+            delete json.children;
+        }
+        return compatibility;
+    }
+};
+
+//src/core/connect.js
+_p[11] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Module = _p.r(20);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        // 连线提供方
+        var _connectProviders = {};
+        function register(name, provider) {
+            _connectProviders[name] = provider;
+        }
+        register("default", function(node, parent, connection) {
+            connection.setPathData([ "M", parent.getLayoutVertexOut(), "L", node.getLayoutVertexIn() ]);
+        });
+        kity.extendClass(MinderNode, {
+            /**
+         * @private
+         * @method getConnect()
+         * @for MinderNode
+         * @description 获取当前节点的连线类型
+         *
+         * @grammar getConnect() => {string}
+         */
+            getConnect: function() {
+                return this.data.connect || "default";
+            },
+            getConnectProvider: function() {
+                return _connectProviders[this.getConnect()] || _connectProviders["default"];
+            },
+            /**
+         * @private
+         * @method getConnection()
+         * @for MinderNode
+         * @description 获取当前节点的连线对象
+         *
+         * @grammar getConnection() => {kity.Path}
+         */
+            getConnection: function() {
+                return this._connection || null;
+            }
+        });
+        kity.extendClass(Minder, {
+            getConnectContainer: function() {
+                return this._connectContainer;
+            },
+            createConnect: function(node) {
+                if (node.isRoot()) return;
+                var connection = new kity.Path();
+                node._connection = connection;
+                this._connectContainer.addShape(connection);
+                this.updateConnect(node);
+            },
+            removeConnect: function(node) {
+                var me = this;
+                node.traverse(function(node) {
+                    me._connectContainer.removeShape(node._connection);
+                    node._connection = null;
+                });
+            },
+            updateConnect: function(node) {
+                var connection = node._connection;
+                var parent = node.parent;
+                if (!parent || !connection) return;
+                if (parent.isCollapsed()) {
+                    connection.setVisible(false);
+                    return;
+                }
+                connection.setVisible(true);
+                var provider = node.getConnectProvider();
+                var strokeColor = node.getStyle("connect-color") || "white", strokeWidth = node.getStyle("connect-width") || 2;
+                connection.stroke(strokeColor, strokeWidth);
+                provider(node, parent, connection, strokeWidth, strokeColor);
+                if (strokeWidth % 2 === 0) {
+                    connection.setTranslate(.5, .5);
+                } else {
+                    connection.setTranslate(0, 0);
+                }
+            }
+        });
+        Module.register("Connect", {
+            init: function() {
+                this._connectContainer = new kity.Group().setId(utils.uuid("minder_connect_group"));
+                this.getRenderContainer().prependShape(this._connectContainer);
+            },
+            events: {
+                nodeattach: function(e) {
+                    this.createConnect(e.node);
+                },
+                nodedetach: function(e) {
+                    this.removeConnect(e.node);
+                },
+                "layoutapply layoutfinish noderender": function(e) {
+                    this.updateConnect(e.node);
+                }
+            }
+        });
+        exports.register = register;
+    }
+};
+
+//src/core/data.js
+_p[12] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var MinderEvent = _p.r(13);
+        var compatibility = _p.r(10);
+        var Promise = _p.r(25);
+        var protocols = {};
+        function registerProtocol(name, protocol) {
+            protocols[name] = protocol;
+            for (var pname in protocols) {
+                if (protocols.hasOwnProperty(pname)) {
+                    protocols[pname] = protocols[pname];
+                    protocols[pname].name = pname;
+                }
+            }
+        }
+        function getRegisterProtocol(name) {
+            return name === undefined ? protocols : protocols[name] || null;
+        }
+        exports.registerProtocol = registerProtocol;
+        exports.getRegisterProtocol = getRegisterProtocol;
+        // 导入导出
+        kity.extendClass(Minder, {
+            // 自动导入
+            setup: function(target) {
+                if (typeof target == "string") {
+                    target = document.querySelector(target);
+                }
+                if (!target) return;
+                var protocol = target.getAttribute("minder-data-type");
+                if (protocol in protocols) {
+                    var data = target.textContent;
+                    target.textContent = null;
+                    this.renderTo(target);
+                    this.importData(protocol, data);
+                }
+                return this;
+            },
+            /**
+         * @method exportJson()
+         * @for Minder
+         * @description
+         *     导出当前脑图数据为 JSON 对象,导出的数据格式请参考 [Data](data) 章节。
+         * @grammar exportJson() => {plain}
+         */
+            exportJson: function() {
+                /* 导出 node 上整棵树的数据为 JSON */
+                function exportNode(node) {
+                    var exported = {};
+                    exported.data = node.getData();
+                    var childNodes = node.getChildren();
+                    exported.children = [];
+                    for (var i = 0; i < childNodes.length; i++) {
+                        exported.children.push(exportNode(childNodes[i]));
+                    }
+                    return exported;
+                }
+                var json = {
+                    root: exportNode(this.getRoot())
+                };
+                json.template = this.getTemplate();
+                json.theme = this.getTheme();
+                json.version = Minder.version;
+                return JSON.parse(JSON.stringify(json));
+            },
+            /**
+         * function Text2Children(MinderNode, String) 
+         * @param {MinderNode} node 要导入数据的节点
+         * @param {String} text 导入的text数据
+         * @Desc: 用于批量插入子节点,并不会修改被插入的父节点
+         * @Editor: Naixor
+         * @Date: 2015.9.21
+         * @example: 用于批量导入如下类型的节点
+         *      234
+         *      3456346 asadf
+         *          12312414
+         *              wereww
+         *          12314
+         *      1231412
+         *      13123    
+         */
+            Text2Children: function(node, text) {
+                if (!(node instanceof kityminder.Node)) {
+                    return;
+                }
+                var children = [], jsonMap = {}, level = 0;
+                var LINE_SPLITTER = /\r|\n|\r\n/, TAB_REGEXP = /^(\t|\x20{4})/;
+                var lines = text.split(LINE_SPLITTER), line = "", jsonNode, i = 0;
+                var minder = this;
+                function isEmpty(line) {
+                    return line === "" && !/\S/.test(line);
+                }
+                function getNode(line) {
+                    return {
+                        data: {
+                            text: line.replace(/^(\t|\x20{4})+/, "").replace(/(\t|\x20{4})+$/, "")
+                        },
+                        children: []
+                    };
+                }
+                function getLevel(text) {
+                    var level = 0;
+                    while (TAB_REGEXP.test(text)) {
+                        text = text.replace(TAB_REGEXP, "");
+                        level++;
+                    }
+                    return level;
+                }
+                function addChild(parent, node) {
+                    parent.children.push(node);
+                }
+                function importChildren(node, children) {
+                    for (var i = 0, l = children.length; i < l; i++) {
+                        var childNode = minder.createNode(null, node);
+                        childNode.setData("text", children[i].data.text || "");
+                        importChildren(childNode, children[i].children);
+                    }
+                }
+                while ((line = lines[i++]) !== undefined) {
+                    line = line.replace(/&nbsp;/g, "");
+                    if (isEmpty(line)) continue;
+                    level = getLevel(line);
+                    jsonNode = getNode(line);
+                    if (level === 0) {
+                        jsonMap = {};
+                        children.push(jsonNode);
+                        jsonMap[0] = children[children.length - 1];
+                    } else {
+                        if (!jsonMap[level - 1]) {
+                            throw new Error("Invalid local format");
+                        }
+                        addChild(jsonMap[level - 1], jsonNode);
+                        jsonMap[level] = jsonNode;
+                    }
+                }
+                importChildren(node, children);
+                minder.refresh();
+            },
+            /**
+         * @method exportNode(MinderNode)
+         * @param  {MinderNode} node 当前要被导出的节点
+         * @return {Object}      返回只含有data和children的Object
+         * @Editor: Naixor
+         * @Date: 2015.9.22
+         */
+            exportNode: function(node) {
+                var exported = {};
+                exported.data = node.getData();
+                var childNodes = node.getChildren();
+                exported.children = [];
+                for (var i = 0; i < childNodes.length; i++) {
+                    exported.children.push(this.exportNode(childNodes[i]));
+                }
+                return exported;
+            },
+            /**
+         * @method importNode()
+         * @description 根据纯json {data, children}数据转换成为脑图节点
+         * @Editor: Naixor
+         * @Date: 2015.9.20
+         */
+            importNode: function(node, json) {
+                var data = json.data;
+                node.data = {};
+                for (var field in data) {
+                    node.setData(field, data[field]);
+                }
+                var childrenTreeData = json.children || [];
+                for (var i = 0; i < childrenTreeData.length; i++) {
+                    var childNode = this.createNode(null, node);
+                    this.importNode(childNode, childrenTreeData[i]);
+                }
+                return node;
+            },
+            /**
+         * @method importJson()
+         * @for Minder
+         * @description 导入脑图数据,数据为 JSON 对象,具体的数据字段形式请参考 [Data](data) 章节。
+         *
+         * @grammar importJson(json) => {this}
+         *
+         * @param {plain} json 要导入的数据
+         */
+            importJson: function(json) {
+                if (!json) return;
+                /**
+             * @event preimport
+             * @for Minder
+             * @when 导入数据之前
+             */
+                this._fire(new MinderEvent("preimport", null, false));
+                // 删除当前所有节点
+                while (this._root.getChildren().length) {
+                    this.removeNode(this._root.getChildren()[0]);
+                }
+                json = compatibility(json);
+                this.importNode(this._root, json.root);
+                this.setTemplate(json.template || "default");
+                this.setTheme(json.theme || null);
+                this.refresh();
+                /**
+             * @event import,contentchange,interactchange
+             * @for Minder
+             * @when 导入数据之后
+             */
+                this.fire("import");
+                this._firePharse({
+                    type: "contentchange"
+                });
+                this._interactChange();
+                return this;
+            },
+            /**
+         * @method exportData()
+         * @for Minder
+         * @description 使用指定使用的数据协议,导入脑图数据
+         *
+         * @grammar exportData(protocol) => Promise<data>
+         *
+         * @param {string} protocol 指定的数据协议(默认内置五种数据协议 `json`、`text`、`markdown`、`svg` 和 `png`)
+         */
+            exportData: function(protocolName, option) {
+                var json, protocol;
+                json = this.exportJson();
+                // 指定了协议进行导出,需要检测协议是否支持
+                if (protocolName) {
+                    protocol = protocols[protocolName];
+                    if (!protocol || !protocol.encode) {
+                        return Promise.reject(new Error("Not supported protocol:" + protocolName));
+                    }
+                }
+                // 导出前抛个事件
+                this._fire(new MinderEvent("beforeexport", {
+                    json: json,
+                    protocolName: protocolName,
+                    protocol: protocol
+                }));
+                return Promise.resolve(protocol.encode(json, this, option));
+            },
+            /**
+         * @method importData()
+         * @for Minder
+         * @description 使用指定的数据协议,导入脑图数据,覆盖当前实例的脑图
+         *
+         * @grammar importData(protocol, callback) => Promise<json>
+         *
+         * @param {string} protocol 指定的用于解析数据的数据协议(默认内置三种数据协议 `json`、`text` 和 `markdown` 的支持)
+         * @param {any} data 要导入的数据
+         */
+            importData: function(protocolName, data, option) {
+                var json, protocol;
+                var minder = this;
+                // 指定了协议进行导入,需要检测协议是否支持
+                if (protocolName) {
+                    protocol = protocols[protocolName];
+                    if (!protocol || !protocol.decode) {
+                        return Promise.reject(new Error("Not supported protocol:" + protocolName));
+                    }
+                }
+                var params = {
+                    local: data,
+                    protocolName: protocolName,
+                    protocol: protocol
+                };
+                // 导入前抛事件
+                this._fire(new MinderEvent("beforeimport", params));
+                return Promise.resolve(protocol.decode(data, this, option)).then(function(json) {
+                    minder.importJson(json);
+                    return json;
+                });
+            },
+            /**
+         * @method decodeData()
+         * @for Minder
+         * @description 使用指定的数据协议,解析为脑图数据,与 importData 的区别在于:不覆盖当前实例的脑图
+         *
+         * @grammar decodeData(protocol, callback) => Promise<json>
+         *
+         * @param {string} protocol 指定的用于解析数据的数据协议(默认内置三种数据协议 `json`、`text` 和 `markdown` 的支持)
+         * @param {any} data 要导入的数据
+         */
+            decodeData: function(protocolName, data, option) {
+                var json, protocol;
+                var minder = this;
+                // 指定了协议进行导入,需要检测协议是否支持
+                if (protocolName) {
+                    protocol = protocols[protocolName];
+                    if (!protocol || !protocol.decode) {
+                        return Promise.reject(new Error("Not supported protocol:" + protocolName));
+                    }
+                }
+                var params = {
+                    local: data,
+                    protocolName: protocolName,
+                    protocol: protocol
+                };
+                // 导入前抛事件
+                this._fire(new MinderEvent("beforeimport", params));
+                return Promise.resolve(protocol.decode(data, this, option));
+            }
+        });
+    }
+};
+
+//src/core/event.js
+_p[13] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        /**
+     * @class MinderEvent
+     * @description 表示一个脑图中发生的事件
+     */
+        var MinderEvent = kity.createClass("MindEvent", {
+            constructor: function(type, params, canstop) {
+                params = params || {};
+                if (params.getType && params.getType() == "ShapeEvent") {
+                    /**
+                 * @property kityEvent
+                 * @for MinderEvent
+                 * @description 如果事件是从一个 kity 的事件派生的,会有 kityEvent 属性指向原来的 kity 事件
+                 * @type {KityEvent}
+                 */
+                    this.kityEvent = params;
+                    /**
+                 * @property originEvent
+                 * @for MinderEvent
+                 * @description 如果事件是从原声 Dom 事件派生的(如 click、mousemove 等),会有 originEvent 指向原来的 Dom 事件
+                 * @type {DomEvent}
+                 */
+                    this.originEvent = params.originEvent;
+                } else if (params.target && params.preventDefault) {
+                    this.originEvent = params;
+                } else {
+                    kity.Utils.extend(this, params);
+                }
+                /**
+             * @property type
+             * @for MinderEvent
+             * @description 事件的类型,如 `click`、`contentchange` 等
+             * @type {string}
+             */
+                this.type = type;
+                this._canstop = canstop || false;
+            },
+            /**
+         * @method getPosition()
+         * @for MinderEvent
+         * @description 如果事件是从一个 kity 事件派生的,会有 `getPosition()` 获取事件发生的坐标
+         *
+         * @grammar getPosition(refer) => {kity.Point}
+         *
+         * @param {string|kity.Shape} refer
+         *     参照的坐标系,
+         *     `"screen"` - 以浏览器屏幕为参照坐标系
+         *     `"minder"` - (默认)以脑图画布为参照坐标系
+         *     `{kity.Shape}` - 指定以某个 kity 图形为参照坐标系
+         */
+            getPosition: function(refer) {
+                if (!this.kityEvent) return;
+                if (!refer || refer == "minder") {
+                    return this.kityEvent.getPosition(this.minder.getRenderContainer());
+                }
+                return this.kityEvent.getPosition.call(this.kityEvent, refer);
+            },
+            /**
+         * @method getTargetNode()
+         * @for MinderEvent
+         * @description 当发生的事件是鼠标事件时,获取事件位置命中的脑图节点
+         *
+         * @grammar getTargetNode() => {MinderNode}
+         */
+            getTargetNode: function() {
+                var findShape = this.kityEvent && this.kityEvent.targetShape;
+                if (!findShape) return null;
+                while (!findShape.minderNode && findShape.container) {
+                    findShape = findShape.container;
+                }
+                var node = findShape.minderNode;
+                if (node && findShape.getOpacity() < 1) return null;
+                return node || null;
+            },
+            /**
+         * @method stopPropagation()
+         * @for MinderEvent
+         * @description 当发生的事件是鼠标事件时,获取事件位置命中的脑图节点
+         *
+         * @grammar getTargetNode() => {MinderNode}
+         */
+            stopPropagation: function() {
+                this._stoped = true;
+            },
+            stopPropagationImmediately: function() {
+                this._immediatelyStoped = true;
+                this._stoped = true;
+            },
+            shouldStopPropagation: function() {
+                return this._canstop && this._stoped;
+            },
+            shouldStopPropagationImmediately: function() {
+                return this._canstop && this._immediatelyStoped;
+            },
+            preventDefault: function() {
+                this.originEvent.preventDefault();
+            },
+            isRightMB: function() {
+                var isRightMB = false;
+                if (!this.originEvent) {
+                    return false;
+                }
+                if ("which" in this.originEvent) isRightMB = this.originEvent.which == 3; else if ("button" in this.originEvent) isRightMB = this.originEvent.button == 2;
+                return isRightMB;
+            },
+            getKeyCode: function() {
+                var evt = this.originEvent;
+                return evt.keyCode || evt.which;
+            }
+        });
+        Minder.registerInitHook(function(option) {
+            this._initEvents();
+        });
+        kity.extendClass(Minder, {
+            _initEvents: function() {
+                this._eventCallbacks = {};
+            },
+            _resetEvents: function() {
+                this._initEvents();
+                this._bindEvents();
+            },
+            _bindEvents: function() {
+                /* jscs:disable maximumLineLength */
+                this._paper.on("click dblclick mousedown contextmenu mouseup mousemove mouseover mousewheel DOMMouseScroll touchstart touchmove touchend dragenter dragleave drop", this._firePharse.bind(this));
+                if (window) {
+                    window.addEventListener("resize", this._firePharse.bind(this));
+                }
+            },
+            /**
+         * @method dispatchKeyEvent
+         * @description 派发键盘(相关)事件到脑图实例上,让实例的模块处理
+         * @grammar dispatchKeyEvent(e) => {this}
+         * @param  {Event} e 原生的 Dom 事件对象
+         */
+            dispatchKeyEvent: function(e) {
+                this._firePharse(e);
+            },
+            _firePharse: function(e) {
+                var beforeEvent, preEvent, executeEvent;
+                if (e.type == "DOMMouseScroll") {
+                    e.type = "mousewheel";
+                    e.wheelDelta = e.originEvent.wheelDelta = e.originEvent.detail * -10;
+                    e.wheelDeltaX = e.originEvent.mozMovementX;
+                    e.wheelDeltaY = e.originEvent.mozMovementY;
+                }
+                beforeEvent = new MinderEvent("before" + e.type, e, true);
+                if (this._fire(beforeEvent)) {
+                    return;
+                }
+                preEvent = new MinderEvent("pre" + e.type, e, true);
+                executeEvent = new MinderEvent(e.type, e, true);
+                if (this._fire(preEvent) || this._fire(executeEvent)) this._fire(new MinderEvent("after" + e.type, e, false));
+            },
+            _interactChange: function(e) {
+                var me = this;
+                if (me._interactScheduled) return;
+                setTimeout(function() {
+                    me._fire(new MinderEvent("interactchange"));
+                    me._interactScheduled = false;
+                }, 100);
+                me._interactScheduled = true;
+            },
+            _listen: function(type, callback) {
+                var callbacks = this._eventCallbacks[type] || (this._eventCallbacks[type] = []);
+                callbacks.push(callback);
+            },
+            _fire: function(e) {
+                /**
+             * @property minder
+             * @description 产生事件的 Minder 对象
+             * @for MinderShape
+             * @type {Minder}
+             */
+                e.minder = this;
+                var status = this.getStatus();
+                var callbacks = this._eventCallbacks[e.type.toLowerCase()] || [];
+                if (status) {
+                    callbacks = callbacks.concat(this._eventCallbacks[status + "." + e.type.toLowerCase()] || []);
+                }
+                if (callbacks.length === 0) {
+                    return;
+                }
+                var lastStatus = this.getStatus();
+                for (var i = 0; i < callbacks.length; i++) {
+                    callbacks[i].call(this, e);
+                    /* this.getStatus() != lastStatus ||*/
+                    if (e.shouldStopPropagationImmediately()) {
+                        break;
+                    }
+                }
+                return e.shouldStopPropagation();
+            },
+            on: function(name, callback) {
+                var km = this;
+                name.split(/\s+/).forEach(function(n) {
+                    km._listen(n.toLowerCase(), callback);
+                });
+                return this;
+            },
+            off: function(name, callback) {
+                var types = name.split(/\s+/);
+                var i, j, callbacks, removeIndex;
+                for (i = 0; i < types.length; i++) {
+                    callbacks = this._eventCallbacks[types[i].toLowerCase()];
+                    if (callbacks) {
+                        removeIndex = null;
+                        for (j = 0; j < callbacks.length; j++) {
+                            if (callbacks[j] == callback) {
+                                removeIndex = j;
+                            }
+                        }
+                        if (removeIndex !== null) {
+                            callbacks.splice(removeIndex, 1);
+                        }
+                    }
+                }
+            },
+            fire: function(type, params) {
+                var e = new MinderEvent(type, params);
+                this._fire(e);
+                return this;
+            }
+        });
+        module.exports = MinderEvent;
+    }
+};
+
+//src/core/focus.js
+_p[14] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Minder = _p.r(19);
+        Minder.registerInitHook(function() {
+            this.on("beforemousedown", function(e) {
+                this.focus();
+                e.preventDefault();
+            });
+            this.on("paperrender", function() {
+                this.focus();
+            });
+        });
+        kity.extendClass(Minder, {
+            focus: function() {
+                if (!this.isFocused()) {
+                    var renderTarget = this._renderTarget;
+                    renderTarget.classList.add("focus");
+                    this.renderNodeBatch(this.getSelectedNodes());
+                }
+                this.fire("focus");
+                return this;
+            },
+            blur: function() {
+                if (this.isFocused()) {
+                    var renderTarget = this._renderTarget;
+                    renderTarget.classList.remove("focus");
+                    this.renderNodeBatch(this.getSelectedNodes());
+                }
+                this.fire("blur");
+                return this;
+            },
+            isFocused: function() {
+                var renderTarget = this._renderTarget;
+                return renderTarget && renderTarget.classList.contains("focus");
+            }
+        });
+    }
+};
+
+//src/core/keymap.js
+_p[15] = {
+    value: function(require, exports, module) {
+        var keymap = {
+            Backspace: 8,
+            Tab: 9,
+            Enter: 13,
+            Shift: 16,
+            Control: 17,
+            Alt: 18,
+            CapsLock: 20,
+            Esc: 27,
+            Spacebar: 32,
+            PageUp: 33,
+            PageDown: 34,
+            End: 35,
+            Home: 36,
+            Insert: 45,
+            Left: 37,
+            Up: 38,
+            Right: 39,
+            Down: 40,
+            direction: {
+                37: 1,
+                38: 1,
+                39: 1,
+                40: 1
+            },
+            Del: 46,
+            NumLock: 144,
+            Cmd: 91,
+            CmdFF: 224,
+            F1: 112,
+            F2: 113,
+            F3: 114,
+            F4: 115,
+            F5: 116,
+            F6: 117,
+            F7: 118,
+            F8: 119,
+            F9: 120,
+            F10: 121,
+            F11: 122,
+            F12: 123,
+            "`": 192,
+            "=": 187,
+            "-": 189,
+            "/": 191,
+            ".": 190,
+            controlKeys: {
+                16: 1,
+                17: 1,
+                18: 1,
+                20: 1,
+                91: 1,
+                224: 1
+            },
+            notContentChange: {
+                13: 1,
+                9: 1,
+                33: 1,
+                34: 1,
+                35: 1,
+                36: 1,
+                16: 1,
+                17: 1,
+                18: 1,
+                20: 1,
+                91: 1,
+                //上下左右
+                37: 1,
+                38: 1,
+                39: 1,
+                40: 1,
+                113: 1,
+                114: 1,
+                115: 1,
+                144: 1,
+                27: 1
+            },
+            isSelectedNodeKey: {
+                //上下左右
+                37: 1,
+                38: 1,
+                39: 1,
+                40: 1,
+                13: 1,
+                9: 1
+            }
+        };
+        // 小写适配
+        for (var key in keymap) {
+            if (keymap.hasOwnProperty(key)) {
+                keymap[key.toLowerCase()] = keymap[key];
+            }
+        }
+        var aKeyCode = 65;
+        var aCharCode = "a".charCodeAt(0);
+        // letters
+        "abcdefghijklmnopqrstuvwxyz".split("").forEach(function(letter) {
+            keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode);
+        });
+        // numbers
+        var n = 9;
+        do {
+            keymap[n.toString()] = n + 48;
+        } while (--n);
+        module.exports = keymap;
+    }
+};
+
+//src/core/keyreceiver.js
+_p[16] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        function listen(element, type, handler) {
+            type.split(" ").forEach(function(name) {
+                element.addEventListener(name, handler, false);
+            });
+        }
+        Minder.registerInitHook(function(option) {
+            this.setDefaultOptions({
+                enableKeyReceiver: true
+            });
+            if (this.getOption("enableKeyReceiver")) {
+                this.on("paperrender", function() {
+                    this._initKeyReceiver();
+                });
+            }
+        });
+        kity.extendClass(Minder, {
+            _initKeyReceiver: function() {
+                if (this._keyReceiver) return;
+                var receiver = this._keyReceiver = document.createElement("input");
+                receiver.classList.add("km-receiver");
+                var renderTarget = this._renderTarget;
+                renderTarget.appendChild(receiver);
+                var minder = this;
+                listen(receiver, "keydown keyup keypress copy paste blur focus input", function(e) {
+                    switch (e.type) {
+                      case "blur":
+                        minder.blur();
+                        break;
+
+                      case "focus":
+                        minder.focus();
+                        break;
+
+                      case "input":
+                        receiver.value = null;
+                        break;
+                    }
+                    minder._firePharse(e);
+                    e.preventDefault();
+                });
+                this.on("focus", function() {
+                    receiver.select();
+                    receiver.focus();
+                });
+                this.on("blur", function() {
+                    receiver.blur();
+                });
+                if (this.isFocused()) {
+                    receiver.select();
+                    receiver.focus();
+                }
+            }
+        });
+    }
+};
+
+//src/core/kity.js
+/**
+ * @fileOverview
+ *
+ * Kity 引入
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[17] = {
+    value: function(require, exports, module) {
+        module.exports = window.kity;
+    }
+};
+
+//src/core/layout.js
+_p[18] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var MinderEvent = _p.r(13);
+        var Command = _p.r(9);
+        var _layouts = {};
+        var _defaultLayout;
+        function register(name, layout) {
+            _layouts[name] = layout;
+            _defaultLayout = _defaultLayout || name;
+        }
+        /**
+     * @class Layout 布局基类,具体布局需要从该类派生
+     */
+        var Layout = kity.createClass("Layout", {
+            /**
+         * @abstract
+         *
+         * 子类需要实现的布局算法,该算法输入一个节点,排布该节点的子节点(相对父节点的变换)
+         *
+         * @param  {MinderNode} node 需要布局的节点
+         *
+         * @example
+         *
+         * doLayout: function(node) {
+         *     var children = node.getChildren();
+         *     // layout calculation
+         *     children[i].setLayoutTransform(new kity.Matrix().translate(x, y));
+         * }
+         */
+            doLayout: function(parent, children) {
+                throw new Error("Not Implement: Layout.doLayout()");
+            },
+            /**
+         * 对齐指定的节点
+         *
+         * @param {Array<MinderNode>} nodes 要对齐的节点
+         * @param {string} border 对齐边界,允许取值 left, right, top, bottom
+         *
+         */
+            align: function(nodes, border, offset) {
+                var me = this;
+                offset = offset || 0;
+                nodes.forEach(function(node) {
+                    var tbox = me.getTreeBox([ node ]);
+                    var matrix = node.getLayoutTransform();
+                    switch (border) {
+                      case "left":
+                        return matrix.translate(offset - tbox.left, 0);
+
+                      case "right":
+                        return matrix.translate(offset - tbox.right, 0);
+
+                      case "top":
+                        return matrix.translate(0, offset - tbox.top);
+
+                      case "bottom":
+                        return matrix.translate(0, offset - tbox.bottom);
+                    }
+                });
+            },
+            stack: function(nodes, axis, distance) {
+                var me = this;
+                var position = 0;
+                distance = distance || function(node, next, axis) {
+                    return node.getStyle({
+                        x: "margin-right",
+                        y: "margin-bottom"
+                    }[axis]) + next.getStyle({
+                        x: "margin-left",
+                        y: "margin-top"
+                    }[axis]);
+                };
+                nodes.forEach(function(node, index, nodes) {
+                    var tbox = me.getTreeBox([ node ]);
+                    var size = {
+                        x: tbox.width,
+                        y: tbox.height
+                    }[axis];
+                    var offset = {
+                        x: tbox.left,
+                        y: tbox.top
+                    }[axis];
+                    var matrix = node.getLayoutTransform();
+                    if (axis == "x") {
+                        matrix.translate(position - offset, 0);
+                    } else {
+                        matrix.translate(0, position - offset);
+                    }
+                    position += size;
+                    if (nodes[index + 1]) position += distance(node, nodes[index + 1], axis);
+                });
+                return position;
+            },
+            move: function(nodes, dx, dy) {
+                nodes.forEach(function(node) {
+                    node.getLayoutTransform().translate(dx, dy);
+                });
+            },
+            /**
+         * 工具方法:获取给点的节点所占的布局区域
+         *
+         * @param  {MinderNode[]} nodes 需要计算的节点
+         *
+         * @return {Box} 计算结果
+         */
+            getBranchBox: function(nodes) {
+                var box = new kity.Box();
+                var i, node, matrix, contentBox;
+                for (i = 0; i < nodes.length; i++) {
+                    node = nodes[i];
+                    matrix = node.getLayoutTransform();
+                    contentBox = node.getContentBox();
+                    box = box.merge(matrix.transformBox(contentBox));
+                }
+                return box;
+            },
+            /**
+         * 工具方法:计算给定的节点的子树所占的布局区域
+         *
+         * @param  {MinderNode} nodes 需要计算的节点
+         *
+         * @return {Box} 计算的结果
+         */
+            getTreeBox: function(nodes) {
+                var i, node, matrix, treeBox;
+                var box = new kity.Box();
+                if (!(nodes instanceof Array)) nodes = [ nodes ];
+                for (i = 0; i < nodes.length; i++) {
+                    node = nodes[i];
+                    matrix = node.getLayoutTransform();
+                    treeBox = node.getContentBox();
+                    if (node.isExpanded() && node.children.length) {
+                        treeBox = treeBox.merge(this.getTreeBox(node.children));
+                    }
+                    box = box.merge(matrix.transformBox(treeBox));
+                }
+                return box;
+            },
+            getOrderHint: function(node) {
+                return [];
+            }
+        });
+        Layout.register = register;
+        Minder.registerInitHook(function(options) {
+            this.refresh();
+        });
+        /**
+     * 布局支持池子管理
+     */
+        utils.extend(Minder, {
+            getLayoutList: function() {
+                return _layouts;
+            },
+            getLayoutInstance: function(name) {
+                var LayoutClass = _layouts[name];
+                if (!LayoutClass) throw new Error("Missing Layout: " + name);
+                var layout = new LayoutClass();
+                return layout;
+            }
+        });
+        /**
+     * MinderNode 上的布局支持
+     */
+        kity.extendClass(MinderNode, {
+            /**
+         * 获得当前节点的布局名称
+         *
+         * @return {String}
+         */
+            getLayout: function() {
+                var layout = this.getData("layout");
+                layout = layout || (this.isRoot() ? _defaultLayout : this.parent.getLayout());
+                return layout;
+            },
+            setLayout: function(name) {
+                if (name) {
+                    if (name == "inherit") {
+                        this.setData("layout");
+                    } else {
+                        this.setData("layout", name);
+                    }
+                }
+                return this;
+            },
+            layout: function(name) {
+                this.setLayout(name).getMinder().layout();
+                return this;
+            },
+            getLayoutInstance: function() {
+                return Minder.getLayoutInstance(this.getLayout());
+            },
+            getOrderHint: function(refer) {
+                return this.parent.getLayoutInstance().getOrderHint(this);
+            },
+            /**
+         * 获取当前节点相对于父节点的布局变换
+         */
+            getLayoutTransform: function() {
+                return this._layoutTransform || new kity.Matrix();
+            },
+            /**
+         * 第一轮布局计算后,获得的全局布局位置
+         *
+         * @return {[type]} [description]
+         */
+            getGlobalLayoutTransformPreview: function() {
+                var pMatrix = this.parent ? this.parent.getLayoutTransform() : new kity.Matrix();
+                var matrix = this.getLayoutTransform();
+                var offset = this.getLayoutOffset();
+                if (offset) {
+                    matrix = matrix.clone().translate(offset.x, offset.y);
+                }
+                return pMatrix.merge(matrix);
+            },
+            getLayoutPointPreview: function() {
+                return this.getGlobalLayoutTransformPreview().transformPoint(new kity.Point());
+            },
+            /**
+         * 获取节点相对于全局的布局变换
+         */
+            getGlobalLayoutTransform: function() {
+                if (this._globalLayoutTransform) {
+                    return this._globalLayoutTransform;
+                } else if (this.parent) {
+                    return this.parent.getGlobalLayoutTransform();
+                } else {
+                    return new kity.Matrix();
+                }
+            },
+            /**
+         * 设置当前节点相对于父节点的布局变换
+         */
+            setLayoutTransform: function(matrix) {
+                this._layoutTransform = matrix;
+                return this;
+            },
+            /**
+         * 设置当前节点相对于全局的布局变换(冗余优化)
+         */
+            setGlobalLayoutTransform: function(matrix) {
+                this.getRenderContainer().setMatrix(this._globalLayoutTransform = matrix);
+                return this;
+            },
+            setVertexIn: function(p) {
+                this._vertexIn = p;
+            },
+            setVertexOut: function(p) {
+                this._vertexOut = p;
+            },
+            getVertexIn: function() {
+                return this._vertexIn || new kity.Point();
+            },
+            getVertexOut: function() {
+                return this._vertexOut || new kity.Point();
+            },
+            getLayoutVertexIn: function() {
+                return this.getGlobalLayoutTransform().transformPoint(this.getVertexIn());
+            },
+            getLayoutVertexOut: function() {
+                return this.getGlobalLayoutTransform().transformPoint(this.getVertexOut());
+            },
+            setLayoutVectorIn: function(v) {
+                this._layoutVectorIn = v;
+                return this;
+            },
+            setLayoutVectorOut: function(v) {
+                this._layoutVectorOut = v;
+                return this;
+            },
+            getLayoutVectorIn: function() {
+                return this._layoutVectorIn || new kity.Vector();
+            },
+            getLayoutVectorOut: function() {
+                return this._layoutVectorOut || new kity.Vector();
+            },
+            getLayoutBox: function() {
+                var matrix = this.getGlobalLayoutTransform();
+                return matrix.transformBox(this.getContentBox());
+            },
+            getLayoutPoint: function() {
+                var matrix = this.getGlobalLayoutTransform();
+                return matrix.transformPoint(new kity.Point());
+            },
+            getLayoutOffset: function() {
+                if (!this.parent) return new kity.Point();
+                // 影响当前节点位置的是父节点的布局
+                var data = this.getData("layout_" + this.parent.getLayout() + "_offset");
+                if (data) return new kity.Point(data.x, data.y);
+                return new kity.Point();
+            },
+            setLayoutOffset: function(p) {
+                if (!this.parent) return this;
+                this.setData("layout_" + this.parent.getLayout() + "_offset", p ? {
+                    x: p.x,
+                    y: p.y
+                } : undefined);
+                return this;
+            },
+            hasLayoutOffset: function() {
+                return !!this.getData("layout_" + this.parent.getLayout() + "_offset");
+            },
+            resetLayoutOffset: function() {
+                return this.setLayoutOffset(null);
+            },
+            getLayoutRoot: function() {
+                if (this.isLayoutRoot()) {
+                    return this;
+                }
+                return this.parent.getLayoutRoot();
+            },
+            isLayoutRoot: function() {
+                return this.getData("layout") || this.isRoot();
+            }
+        });
+        /**
+     * Minder 上的布局支持
+     */
+        kity.extendClass(Minder, {
+            layout: function() {
+                var duration = this.getOption("layoutAnimationDuration");
+                this.getRoot().traverse(function(node) {
+                    // clear last results
+                    node.setLayoutTransform(null);
+                });
+                function layoutNode(node, round) {
+                    // layout all children first
+                    // 剪枝:收起的节点无需计算
+                    if (node.isExpanded() || true) {
+                        node.children.forEach(function(child) {
+                            layoutNode(child, round);
+                        });
+                    }
+                    var layout = node.getLayoutInstance();
+                    // var childrenInFlow = node.getChildren().filter(function(child) {
+                    //     return !child.hasLayoutOffset();
+                    // });
+                    layout.doLayout(node, node.getChildren(), round);
+                }
+                // 第一轮布局
+                layoutNode(this.getRoot(), 1);
+                // 第二轮布局
+                layoutNode(this.getRoot(), 2);
+                var minder = this;
+                this.applyLayoutResult(this.getRoot(), duration, function() {
+                    /**
+                 * 当节点>200, 不使用动画时, 此处逻辑变为同步逻辑, 外部minder.on事件无法
+                 * 被提前录入, 因此增加setTimeout
+                 * @author Naixor
+                 */
+                    setTimeout(function() {
+                        minder.fire("layoutallfinish");
+                    }, 0);
+                });
+                return this.fire("layout");
+            },
+            refresh: function() {
+                this.getRoot().renderTree();
+                this.layout().fire("contentchange")._interactChange();
+                return this;
+            },
+            applyLayoutResult: function(root, duration, callback) {
+                root = root || this.getRoot();
+                var me = this;
+                var complex = root.getComplex();
+                function consume() {
+                    if (!--complex) {
+                        if (callback) {
+                            callback();
+                        }
+                    }
+                }
+                // 节点复杂度大于 100,关闭动画
+                if (complex > 200) duration = 0;
+                function applyMatrix(node, matrix) {
+                    node.setGlobalLayoutTransform(matrix);
+                    me.fire("layoutapply", {
+                        node: node,
+                        matrix: matrix
+                    });
+                }
+                function apply(node, pMatrix) {
+                    var matrix = node.getLayoutTransform().merge(pMatrix.clone());
+                    var lastMatrix = node.getGlobalLayoutTransform() || new kity.Matrix();
+                    var offset = node.getLayoutOffset();
+                    matrix.translate(offset.x, offset.y);
+                    matrix.m.e = Math.round(matrix.m.e);
+                    matrix.m.f = Math.round(matrix.m.f);
+                    // 如果当前有动画,停止动画
+                    if (node._layoutTimeline) {
+                        node._layoutTimeline.stop();
+                        node._layoutTimeline = null;
+                    }
+                    // 如果要求以动画形式来更新,创建动画
+                    if (duration) {
+                        node._layoutTimeline = new kity.Animator(lastMatrix, matrix, applyMatrix).start(node, duration, "ease").on("finish", function() {
+                            //可能性能低的时候会丢帧,手动添加一帧
+                            setTimeout(function() {
+                                applyMatrix(node, matrix);
+                                me.fire("layoutfinish", {
+                                    node: node,
+                                    matrix: matrix
+                                });
+                                consume();
+                            }, 150);
+                        });
+                    } else {
+                        applyMatrix(node, matrix);
+                        me.fire("layoutfinish", {
+                            node: node,
+                            matrix: matrix
+                        });
+                        consume();
+                    }
+                    for (var i = 0; i < node.children.length; i++) {
+                        apply(node.children[i], matrix);
+                    }
+                }
+                apply(root, root.parent ? root.parent.getGlobalLayoutTransform() : new kity.Matrix());
+                return this;
+            }
+        });
+        module.exports = Layout;
+    }
+};
+
+//src/core/minder.js
+/**
+ * @fileOverview
+ *
+ * KityMinder 类,暴露在 window 上的唯一变量
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[19] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var _initHooks = [];
+        var Minder = kity.createClass("Minder", {
+            constructor: function(options) {
+                this._options = utils.extend({}, options);
+                var initHooks = _initHooks.slice();
+                var initHook;
+                while (initHooks.length) {
+                    initHook = initHooks.shift();
+                    if (typeof initHook == "function") {
+                        initHook.call(this, this._options);
+                    }
+                }
+                this.fire("finishInitHook");
+            }
+        });
+        Minder.version = "1.4.43";
+        Minder.registerInitHook = function(hook) {
+            _initHooks.push(hook);
+        };
+        module.exports = Minder;
+    }
+};
+
+//src/core/module.js
+_p[20] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        /* 已注册的模块 */
+        var _modules = {};
+        exports.register = function(name, module) {
+            _modules[name] = module;
+        };
+        /* 模块初始化 */
+        Minder.registerInitHook(function() {
+            this._initModules();
+        });
+        // 模块声明周期维护
+        kity.extendClass(Minder, {
+            _initModules: function() {
+                var modulesPool = _modules;
+                var modulesToLoad = this._options.modules || utils.keys(modulesPool);
+                this._commands = {};
+                this._query = {};
+                this._modules = {};
+                this._rendererClasses = {};
+                var i, name, type, module, moduleDeals, dealCommands, dealEvents, dealRenderers;
+                var me = this;
+                for (i = 0; i < modulesToLoad.length; i++) {
+                    name = modulesToLoad[i];
+                    if (!modulesPool[name]) continue;
+                    // 执行模块初始化,抛出后续处理对象
+                    if (typeof modulesPool[name] == "function") {
+                        moduleDeals = modulesPool[name].call(me);
+                    } else {
+                        moduleDeals = modulesPool[name];
+                    }
+                    this._modules[name] = moduleDeals;
+                    if (!moduleDeals) continue;
+                    if (moduleDeals.defaultOptions) {
+                        me.setDefaultOptions(moduleDeals.defaultOptions);
+                    }
+                    if (moduleDeals.init) {
+                        moduleDeals.init.call(me, this._options);
+                    }
+                    /**
+                 * @Desc: 判断是否支持原生clipboard事件,如果支持,则对pager添加其监听
+                 * @Editor: Naixor
+                 * @Date: 2015.9.20
+                 */
+                    /**
+                 * 由于当前脑图解构问题,clipboard暂时全权交由玩不托管
+                 * @Editor: Naixor
+                 * @Date: 2015.9.24
+                 */
+                    // if (name === 'ClipboardModule' && this.supportClipboardEvent  && !kity.Browser.gecko) {
+                    //     var on = function () {
+                    //         var clipBoardReceiver = this.clipBoardReceiver || document;
+                    //         if (document.addEventListener) {
+                    //             clipBoardReceiver.addEventListener.apply(this, arguments);
+                    //         } else {
+                    //             arguments[0] = 'on' + arguments[0];
+                    //             clipBoardReceiver.attachEvent.apply(this, arguments);
+                    //         }
+                    //     }
+                    //     for (var command in moduleDeals.clipBoardEvents) {
+                    //         on(command, moduleDeals.clipBoardEvents[command]);
+                    //     }
+                    // };
+                    // command加入命令池子
+                    dealCommands = moduleDeals.commands;
+                    for (name in dealCommands) {
+                        this._commands[name.toLowerCase()] = new dealCommands[name]();
+                    }
+                    // 绑定事件
+                    dealEvents = moduleDeals.events;
+                    if (dealEvents) {
+                        for (type in dealEvents) {
+                            me.on(type, dealEvents[type]);
+                        }
+                    }
+                    // 渲染器
+                    dealRenderers = moduleDeals.renderers;
+                    if (dealRenderers) {
+                        for (type in dealRenderers) {
+                            this._rendererClasses[type] = this._rendererClasses[type] || [];
+                            if (utils.isArray(dealRenderers[type])) {
+                                this._rendererClasses[type] = this._rendererClasses[type].concat(dealRenderers[type]);
+                            } else {
+                                this._rendererClasses[type].push(dealRenderers[type]);
+                            }
+                        }
+                    }
+                    //添加模块的快捷键
+                    if (moduleDeals.commandShortcutKeys) {
+                        this.addCommandShortcutKeys(moduleDeals.commandShortcutKeys);
+                    }
+                }
+            },
+            _garbage: function() {
+                this.clearSelect();
+                while (this._root.getChildren().length) {
+                    this._root.removeChild(0);
+                }
+            },
+            destroy: function() {
+                var modules = this._modules;
+                this._resetEvents();
+                this._garbage();
+                for (var key in modules) {
+                    if (!modules[key].destroy) continue;
+                    modules[key].destroy.call(this);
+                }
+            },
+            reset: function() {
+                var modules = this._modules;
+                this._garbage();
+                for (var key in modules) {
+                    if (!modules[key].reset) continue;
+                    modules[key].reset.call(this);
+                }
+            }
+        });
+    }
+};
+
+//src/core/node.js
+_p[21] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        /**
+     * @class MinderNode
+     *
+     * 表示一个脑图节点
+     */
+        var MinderNode = kity.createClass("MinderNode", {
+            /**
+         * 创建一个游离的脑图节点
+         *
+         * @param {String|Object} textOrData
+         *     节点的初始数据或文本
+         */
+            constructor: function(textOrData) {
+                // 指针
+                this.parent = null;
+                this.root = this;
+                this.children = [];
+                // 数据
+                this.data = {
+                    id: utils.guid(),
+                    created: +new Date()
+                };
+                // 绘图容器
+                this.initContainers();
+                if (utils.isString(textOrData)) {
+                    this.setText(textOrData);
+                } else if (utils.isObject(textOrData)) {
+                    utils.extend(this.data, textOrData);
+                }
+            },
+            initContainers: function() {
+                this.rc = new kity.Group().setId(utils.uuid("minder_node"));
+                this.rc.minderNode = this;
+            },
+            /**
+         * 判断节点是否根节点
+         */
+            isRoot: function() {
+                return this.root === this;
+            },
+            /**
+         * 判断节点是否叶子
+         */
+            isLeaf: function() {
+                return this.children.length === 0;
+            },
+            /**
+         * 获取节点的根节点
+         */
+            getRoot: function() {
+                return this.root || this;
+            },
+            /**
+         * 获得节点的父节点
+         */
+            getParent: function() {
+                return this.parent;
+            },
+            getSiblings: function() {
+                var children = this.parent.children;
+                var siblings = [];
+                var self = this;
+                children.forEach(function(child) {
+                    if (child != self) siblings.push(child);
+                });
+                return siblings;
+            },
+            /**
+         * 获得节点的深度
+         */
+            getLevel: function() {
+                var level = 0, ancestor = this.parent;
+                while (ancestor) {
+                    level++;
+                    ancestor = ancestor.parent;
+                }
+                return level;
+            },
+            /**
+         * 获得节点的复杂度(即子树中节点的数量)
+         */
+            getComplex: function() {
+                var complex = 0;
+                this.traverse(function() {
+                    complex++;
+                });
+                return complex;
+            },
+            /**
+         * 获得节点的类型(root|main|sub)
+         */
+            getType: function(type) {
+                this.type = [ "root", "main", "sub" ][Math.min(this.getLevel(), 2)];
+                return this.type;
+            },
+            /**
+         * 判断当前节点是否被测试节点的祖先
+         * @param  {MinderNode}  test 被测试的节点
+         */
+            isAncestorOf: function(test) {
+                var ancestor = test.parent;
+                while (ancestor) {
+                    if (ancestor == this) return true;
+                    ancestor = ancestor.parent;
+                }
+                return false;
+            },
+            getData: function(key) {
+                return key ? this.data[key] : this.data;
+            },
+            setData: function(key, value) {
+                if (typeof key == "object") {
+                    var data = key;
+                    for (key in data) if (data.hasOwnProperty(key)) {
+                        this.data[key] = data[key];
+                    }
+                } else {
+                    this.data[key] = value;
+                }
+                return this;
+            },
+            /**
+         * 设置节点的文本数据
+         * @param {String} text 文本数据
+         */
+            setText: function(text) {
+                return this.data.text = text;
+            },
+            /**
+         * 获取节点的文本数据
+         * @return {String}
+         */
+            getText: function() {
+                return this.data.text || null;
+            },
+            /**
+         * 先序遍历当前节点树
+         * @param  {Function} fn 遍历函数
+         */
+            preTraverse: function(fn, excludeThis) {
+                var children = this.getChildren();
+                if (!excludeThis) fn(this);
+                for (var i = 0; i < children.length; i++) {
+                    children[i].preTraverse(fn);
+                }
+            },
+            /**
+         * 后序遍历当前节点树
+         * @param  {Function} fn 遍历函数
+         */
+            postTraverse: function(fn, excludeThis) {
+                var children = this.getChildren();
+                for (var i = 0; i < children.length; i++) {
+                    children[i].postTraverse(fn);
+                }
+                if (!excludeThis) fn(this);
+            },
+            traverse: function(fn, excludeThis) {
+                return this.postTraverse(fn, excludeThis);
+            },
+            getChildren: function() {
+                return this.children;
+            },
+            getIndex: function() {
+                return this.parent ? this.parent.children.indexOf(this) : -1;
+            },
+            insertChild: function(node, index) {
+                if (index === undefined) {
+                    index = this.children.length;
+                }
+                if (node.parent) {
+                    node.parent.removeChild(node);
+                }
+                node.parent = this;
+                node.root = this.root;
+                this.children.splice(index, 0, node);
+            },
+            appendChild: function(node) {
+                return this.insertChild(node);
+            },
+            prependChild: function(node) {
+                return this.insertChild(node, 0);
+            },
+            removeChild: function(elem) {
+                var index = elem, removed;
+                if (elem instanceof MinderNode) {
+                    index = this.children.indexOf(elem);
+                }
+                if (index >= 0) {
+                    removed = this.children.splice(index, 1)[0];
+                    removed.parent = null;
+                    removed.root = removed;
+                }
+            },
+            clearChildren: function() {
+                this.children = [];
+            },
+            getChild: function(index) {
+                return this.children[index];
+            },
+            getRenderContainer: function() {
+                return this.rc;
+            },
+            getCommonAncestor: function(node) {
+                return MinderNode.getCommonAncestor(this, node);
+            },
+            contains: function(node) {
+                return this == node || this.isAncestorOf(node);
+            },
+            clone: function() {
+                var cloned = new MinderNode();
+                cloned.data = utils.clone(this.data);
+                this.children.forEach(function(child) {
+                    cloned.appendChild(child.clone());
+                });
+                return cloned;
+            },
+            compareTo: function(node) {
+                if (!utils.comparePlainObject(this.data, node.data)) return false;
+                if (!utils.comparePlainObject(this.temp, node.temp)) return false;
+                if (this.children.length != node.children.length) return false;
+                var i = 0;
+                while (this.children[i]) {
+                    if (!this.children[i].compareTo(node.children[i])) return false;
+                    i++;
+                }
+                return true;
+            },
+            getMinder: function() {
+                return this.getRoot().minder;
+            }
+        });
+        MinderNode.getCommonAncestor = function(nodeA, nodeB) {
+            if (nodeA instanceof Array) {
+                return MinderNode.getCommonAncestor.apply(this, nodeA);
+            }
+            switch (arguments.length) {
+              case 1:
+                return nodeA.parent || nodeA;
+
+              case 2:
+                if (nodeA.isAncestorOf(nodeB)) {
+                    return nodeA;
+                }
+                if (nodeB.isAncestorOf(nodeA)) {
+                    return nodeB;
+                }
+                var ancestor = nodeA.parent;
+                while (ancestor && !ancestor.isAncestorOf(nodeB)) {
+                    ancestor = ancestor.parent;
+                }
+                return ancestor;
+
+              default:
+                return Array.prototype.reduce.call(arguments, function(prev, current) {
+                    return MinderNode.getCommonAncestor(prev, current);
+                }, nodeA);
+            }
+        };
+        kity.extendClass(Minder, {
+            getRoot: function() {
+                return this._root;
+            },
+            setRoot: function(root) {
+                this._root = root;
+                root.minder = this;
+            },
+            getAllNode: function() {
+                var nodes = [];
+                this.getRoot().traverse(function(node) {
+                    nodes.push(node);
+                });
+                return nodes;
+            },
+            getNodeById: function(id) {
+                return this.getNodesById([ id ])[0];
+            },
+            getNodesById: function(ids) {
+                var nodes = this.getAllNode();
+                var result = [];
+                nodes.forEach(function(node) {
+                    if (ids.indexOf(node.getData("id")) != -1) {
+                        result.push(node);
+                    }
+                });
+                return result;
+            },
+            createNode: function(textOrData, parent, index) {
+                var node = new MinderNode(textOrData);
+                this.fire("nodecreate", {
+                    node: node,
+                    parent: parent,
+                    index: index
+                });
+                this.appendNode(node, parent, index);
+                return node;
+            },
+            appendNode: function(node, parent, index) {
+                if (parent) parent.insertChild(node, index);
+                this.attachNode(node);
+                return this;
+            },
+            removeNode: function(node) {
+                if (node.parent) {
+                    node.parent.removeChild(node);
+                    this.detachNode(node);
+                    this.fire("noderemove", {
+                        node: node
+                    });
+                }
+            },
+            attachNode: function(node) {
+                var rc = this.getRenderContainer();
+                node.traverse(function(current) {
+                    current.attached = true;
+                    rc.addShape(current.getRenderContainer());
+                });
+                rc.addShape(node.getRenderContainer());
+                this.fire("nodeattach", {
+                    node: node
+                });
+            },
+            detachNode: function(node) {
+                var rc = this.getRenderContainer();
+                node.traverse(function(current) {
+                    current.attached = false;
+                    rc.removeShape(current.getRenderContainer());
+                });
+                this.fire("nodedetach", {
+                    node: node
+                });
+            },
+            getMinderTitle: function() {
+                return this.getRoot().getText();
+            }
+        });
+        module.exports = MinderNode;
+    }
+};
+
+//src/core/option.js
+/**
+ * @fileOverview
+ *
+ * 提供脑图选项支持
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[22] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        Minder.registerInitHook(function(options) {
+            this._defaultOptions = {};
+        });
+        kity.extendClass(Minder, {
+            setDefaultOptions: function(options) {
+                utils.extend(this._defaultOptions, options);
+                return this;
+            },
+            getOption: function(key) {
+                if (key) {
+                    return key in this._options ? this._options[key] : this._defaultOptions[key];
+                } else {
+                    return utils.extend({}, this._defaultOptions, this._options);
+                }
+            },
+            setOption: function(key, value) {
+                this._options[key] = value;
+            }
+        });
+    }
+};
+
+//src/core/paper.js
+/**
+ * @fileOverview
+ *
+ * 初始化渲染容器
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[23] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        Minder.registerInitHook(function() {
+            this._initPaper();
+        });
+        kity.extendClass(Minder, {
+            _initPaper: function() {
+                this._paper = new kity.Paper();
+                this._paper._minder = this;
+                this._paper.getNode().ondragstart = function(e) {
+                    e.preventDefault();
+                };
+                this._paper.shapeNode.setAttribute("transform", "translate(0.5, 0.5)");
+                this._addRenderContainer();
+                this.setRoot(this.createNode());
+                if (this._options.renderTo) {
+                    this.renderTo(this._options.renderTo);
+                }
+            },
+            _addRenderContainer: function() {
+                this._rc = new kity.Group().setId(utils.uuid("minder"));
+                this._paper.addShape(this._rc);
+            },
+            renderTo: function(target) {
+                if (typeof target == "string") {
+                    target = document.querySelector(target);
+                }
+                if (target) {
+                    if (target.tagName.toLowerCase() == "script") {
+                        var newTarget = document.createElement("div");
+                        newTarget.id = target.id;
+                        newTarget.class = target.class;
+                        target.parentNode.insertBefore(newTarget, target);
+                        target.parentNode.removeChild(target);
+                        target = newTarget;
+                    }
+                    target.classList.add("km-view");
+                    this._paper.renderTo(this._renderTarget = target);
+                    this._bindEvents();
+                    this.fire("paperrender");
+                }
+                return this;
+            },
+            getRenderContainer: function() {
+                return this._rc;
+            },
+            getPaper: function() {
+                return this._paper;
+            },
+            getRenderTarget: function() {
+                return this._renderTarget;
+            }
+        });
+    }
+};
+
+//src/core/patch.js
+/**
+ * @fileOverview
+ *
+ * 打补丁
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[24] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Minder = _p.r(19);
+        function insertNode(minder, info, parent, index) {
+            parent = minder.createNode(info.data, parent, index);
+            info.children.forEach(function(childInfo, index) {
+                insertNode(minder, childInfo, parent, index);
+            });
+            return parent;
+        }
+        function applyPatch(minder, patch) {
+            // patch.op - 操作,包括 remove, add, replace
+            // patch.path - 路径,如 '/root/children/1/data'
+            // patch.value - 数据,如 { text: "思路" }
+            var path = patch.path.split("/");
+            path.shift();
+            var changed = path.shift();
+            var node;
+            if (changed == "root") {
+                var dataIndex = path.indexOf("data");
+                if (dataIndex > -1) {
+                    changed = "data";
+                    var dataPath = path.splice(dataIndex + 1);
+                    patch.dataPath = dataPath;
+                } else {
+                    changed = "node";
+                }
+                node = minder.getRoot();
+                var segment, index;
+                while (segment = path.shift()) {
+                    if (segment == "children") continue;
+                    if (typeof index != "undefined") node = node.getChild(index);
+                    index = +segment;
+                }
+                patch.index = index;
+                patch.node = node;
+            }
+            var express = patch.express = [ changed, patch.op ].join(".");
+            switch (express) {
+              case "theme.replace":
+                minder.useTheme(patch.value);
+                break;
+
+              case "template.replace":
+                minder.useTemplate(patch.value);
+                break;
+
+              case "node.add":
+                insertNode(minder, patch.value, patch.node, patch.index).renderTree();
+                minder.layout();
+                break;
+
+              case "node.remove":
+                minder.removeNode(patch.node.getChild(patch.index));
+                minder.layout();
+                break;
+
+              case "data.add":
+              case "data.replace":
+              case "data.remove":
+                var data = patch.node.data;
+                var field;
+                path = patch.dataPath.slice();
+                while (data && path.length > 1) {
+                    field = path.shift();
+                    if (field in data) {
+                        data = data[field];
+                    } else if (patch.op != "remove") {
+                        data = data[field] = {};
+                    }
+                }
+                if (data) {
+                    field = path.shift();
+                    data[field] = patch.value;
+                }
+                if (field == "expandState") {
+                    node.renderTree();
+                } else {
+                    node.render();
+                }
+                minder.layout();
+            }
+            minder.fire("patch", {
+                patch: patch
+            });
+        }
+        kity.extendClass(Minder, {
+            applyPatches: function(patches) {
+                for (var i = 0; i < patches.length; i++) {
+                    applyPatch(this, patches[i]);
+                }
+                this.fire("contentchange");
+                return this;
+            }
+        });
+    }
+};
+
+//src/core/promise.js
+_p[25] = {
+    value: function(require, exports, module) {
+        /*!
+    **  Thenable -- Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
+    **  Copyright (c) 2013-2014 Ralf S. Engelschall <http://engelschall.com>
+    **  Licensed under The MIT License <http://opensource.org/licenses/MIT>
+    **  Source-Code distributed on <http://github.com/rse/thenable>
+    */
+        /*  promise states [Promises/A+ 2.1]  */
+        var STATE_PENDING = 0;
+        /*  [Promises/A+ 2.1.1]  */
+        var STATE_FULFILLED = 1;
+        /*  [Promises/A+ 2.1.2]  */
+        var STATE_REJECTED = 2;
+        /*  [Promises/A+ 2.1.3]  */
+        /*  promise object constructor  */
+        var Promise = function(executor) {
+            /*  optionally support non-constructor/plain-function call  */
+            if (!(this instanceof Promise)) return new Promise(executor);
+            /*  initialize object  */
+            this.id = "Thenable/1.0.7";
+            this.state = STATE_PENDING;
+            /*  initial state  */
+            this.fulfillValue = undefined;
+            /*  initial value  */
+            /*  [Promises/A+ 1.3, 2.1.2.2]  */
+            this.rejectReason = undefined;
+            /*  initial reason */
+            /*  [Promises/A+ 1.5, 2.1.3.2]  */
+            this.onFulfilled = [];
+            /*  initial handlers  */
+            this.onRejected = [];
+            /*  initial handlers  */
+            /*  support optional executor function  */
+            if (typeof executor === "function") executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
+        };
+        /*  Promise API methods  */
+        Promise.prototype = {
+            /*  promise resolving methods  */
+            fulfill: function(value) {
+                return deliver(this, STATE_FULFILLED, "fulfillValue", value);
+            },
+            reject: function(value) {
+                return deliver(this, STATE_REJECTED, "rejectReason", value);
+            },
+            /*  'The then Method' [Promises/A+ 1.1, 1.2, 2.2]  */
+            then: function(onFulfilled, onRejected) {
+                var curr = this;
+                var next = new Promise();
+                /*  [Promises/A+ 2.2.7]  */
+                curr.onFulfilled.push(resolver(onFulfilled, next, "fulfill"));
+                /*  [Promises/A+ 2.2.2/2.2.6]  */
+                curr.onRejected.push(resolver(onRejected, next, "reject"));
+                /*  [Promises/A+ 2.2.3/2.2.6]  */
+                execute(curr);
+                return next;
+            }
+        };
+        Promise.all = function(arr) {
+            return new Promise(function(resolve, reject) {
+                var len = arr.length, i = 0, res = 0, results = [];
+                if (len === 0) {
+                    resolve(results);
+                }
+                while (i < len) {
+                    arr[i].then(function(result) {
+                        results.push(result);
+                        if (++res === len) {
+                            resolve(results);
+                        }
+                    }, function(val) {
+                        reject(val);
+                    });
+                    i++;
+                }
+            });
+        };
+        /*  deliver an action  */
+        var deliver = function(curr, state, name, value) {
+            if (curr.state === STATE_PENDING) {
+                curr.state = state;
+                /*  [Promises/A+ 2.1.2.1, 2.1.3.1]  */
+                curr[name] = value;
+                /*  [Promises/A+ 2.1.2.2, 2.1.3.2]  */
+                execute(curr);
+            }
+            return curr;
+        };
+        /*  execute all handlers  */
+        var execute = function(curr) {
+            if (curr.state === STATE_FULFILLED) execute_handlers(curr, "onFulfilled", curr.fulfillValue); else if (curr.state === STATE_REJECTED) execute_handlers(curr, "onRejected", curr.rejectReason);
+        };
+        /*  execute particular set of handlers  */
+        var execute_handlers = function(curr, name, value) {
+            /* global process: true */
+            /* global setImmediate: true */
+            /* global setTimeout: true */
+            /*  short-circuit processing  */
+            if (curr[name].length === 0) return;
+            /*  iterate over all handlers, exactly once  */
+            var handlers = curr[name];
+            curr[name] = [];
+            /*  [Promises/A+ 2.2.2.3, 2.2.3.3]  */
+            var func = function() {
+                for (var i = 0; i < handlers.length; i++) handlers[i](value);
+            };
+            /*  execute procedure asynchronously  */
+            /*  [Promises/A+ 2.2.4, 3.1]  */
+            if (typeof process === "object" && typeof process.nextTick === "function") process.nextTick(func); else if (typeof setImmediate === "function") setImmediate(func); else setTimeout(func, 0);
+        };
+        /*  generate a resolver function */
+        var resolver = function(cb, next, method) {
+            return function(value) {
+                if (typeof cb !== "function") /*  [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4]  */
+                next[method].call(next, value); else {
+                    var result;
+                    try {
+                        if (value instanceof Promise) {
+                            result = value.then(cb);
+                        } else result = cb(value);
+                    } /*  [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2]  */
+                    catch (e) {
+                        next.reject(e);
+                        /*  [Promises/A+ 2.2.7.2]  */
+                        return;
+                    }
+                    resolve(next, result);
+                }
+            };
+        };
+        /*  'Promise Resolution Procedure'  */
+        /*  [Promises/A+ 2.3]  */
+        var resolve = function(promise, x) {
+            /*  sanity check arguments  */
+            /*  [Promises/A+ 2.3.1]  */
+            if (promise === x) {
+                promise.reject(new TypeError("cannot resolve promise with itself"));
+                return;
+            }
+            /*  surgically check for a 'then' method
+            (mainly to just call the 'getter' of 'then' only once)  */
+            var then;
+            if (typeof x === "object" && x !== null || typeof x === "function") {
+                try {
+                    then = x.then;
+                } /*  [Promises/A+ 2.3.3.1, 3.5]  */
+                catch (e) {
+                    promise.reject(e);
+                    /*  [Promises/A+ 2.3.3.2]  */
+                    return;
+                }
+            }
+            /*  handle own Thenables    [Promises/A+ 2.3.2]
+            and similar 'thenables' [Promises/A+ 2.3.3]  */
+            if (typeof then === "function") {
+                var resolved = false;
+                try {
+                    /*  call retrieved 'then' method */
+                    /*  [Promises/A+ 2.3.3.3]  */
+                    then.call(x, /*  resolvePromise  */
+                    /*  [Promises/A+ 2.3.3.3.1]  */
+                    function(y) {
+                        if (resolved) return;
+                        resolved = true;
+                        /*  [Promises/A+ 2.3.3.3.3]  */
+                        if (y === x) /*  [Promises/A+ 3.6]  */
+                        promise.reject(new TypeError("circular thenable chain")); else resolve(promise, y);
+                    }, /*  rejectPromise  */
+                    /*  [Promises/A+ 2.3.3.3.2]  */
+                    function(r) {
+                        if (resolved) return;
+                        resolved = true;
+                        /*  [Promises/A+ 2.3.3.3.3]  */
+                        promise.reject(r);
+                    });
+                } catch (e) {
+                    if (!resolved) /*  [Promises/A+ 2.3.3.3.3]  */
+                    promise.reject(e);
+                }
+                return;
+            }
+            /*  handle other values  */
+            promise.fulfill(x);
+        };
+        Promise.resolve = function(value) {
+            return new Promise(function(resolve) {
+                resolve(value);
+            });
+        };
+        Promise.reject = function(reason) {
+            return new Promise(function(resolve, reject) {
+                reject(reason);
+            });
+        };
+        /*  export API  */
+        module.exports = Promise;
+    }
+};
+
+//src/core/readonly.js
+/**
+ * @fileOverview
+ *
+ * 只读模式支持
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[26] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Minder = _p.r(19);
+        var MinderEvent = _p.r(13);
+        Minder.registerInitHook(function(options) {
+            if (options.readOnly) {
+                this.setDisabled();
+            }
+        });
+        kity.extendClass(Minder, {
+            disable: function() {
+                var me = this;
+                //禁用命令
+                me.bkqueryCommandState = me.queryCommandState;
+                me.bkqueryCommandValue = me.queryCommandValue;
+                me.queryCommandState = function(type) {
+                    var cmd = this._getCommand(type);
+                    if (cmd && cmd.enableReadOnly) {
+                        return me.bkqueryCommandState.apply(me, arguments);
+                    }
+                    return -1;
+                };
+                me.queryCommandValue = function(type) {
+                    var cmd = this._getCommand(type);
+                    if (cmd && cmd.enableReadOnly) {
+                        return me.bkqueryCommandValue.apply(me, arguments);
+                    }
+                    return null;
+                };
+                this.setStatus("readonly");
+                me._interactChange();
+            },
+            enable: function() {
+                var me = this;
+                if (me.bkqueryCommandState) {
+                    me.queryCommandState = me.bkqueryCommandState;
+                    delete me.bkqueryCommandState;
+                }
+                if (me.bkqueryCommandValue) {
+                    me.queryCommandValue = me.bkqueryCommandValue;
+                    delete me.bkqueryCommandValue;
+                }
+                this.setStatus("normal");
+                me._interactChange();
+            }
+        });
+    }
+};
+
+//src/core/render.js
+_p[27] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Renderer = kity.createClass("Renderer", {
+            constructor: function(node) {
+                this.node = node;
+            },
+            create: function(node) {
+                throw new Error("Not implement: Renderer.create()");
+            },
+            shouldRender: function(node) {
+                return true;
+            },
+            watchChange: function(data) {
+                var changed;
+                if (this.watchingData === undefined) {
+                    changed = true;
+                } else if (this.watchingData != data) {
+                    changed = true;
+                } else {
+                    changed = false;
+                }
+                this.watchingData = data;
+            },
+            shouldDraw: function(node) {
+                return true;
+            },
+            update: function(shape, node, box) {
+                if (this.shouldDraw()) this.draw(shape, node);
+                return this.place(shape, node, box);
+            },
+            draw: function(shape, node) {
+                throw new Error("Not implement: Renderer.draw()");
+            },
+            place: function(shape, node, box) {
+                throw new Error("Not implement: Renderer.place()");
+            },
+            getRenderShape: function() {
+                return this._renderShape || null;
+            },
+            setRenderShape: function(shape) {
+                this._renderShape = shape;
+            }
+        });
+        function createMinderExtension() {
+            function createRendererForNode(node, registered) {
+                var renderers = [];
+                [ "center", "left", "right", "top", "bottom", "outline", "outside" ].forEach(function(section) {
+                    var before = "before" + section;
+                    var after = "after" + section;
+                    if (registered[before]) {
+                        renderers = renderers.concat(registered[before]);
+                    }
+                    if (registered[section]) {
+                        renderers = renderers.concat(registered[section]);
+                    }
+                    if (registered[after]) {
+                        renderers = renderers.concat(registered[after]);
+                    }
+                });
+                node._renderers = renderers.map(function(Renderer) {
+                    return new Renderer(node);
+                });
+            }
+            return {
+                renderNodeBatch: function(nodes) {
+                    var rendererClasses = this._rendererClasses;
+                    var lastBoxes = [];
+                    var rendererCount = 0;
+                    var i, j, renderer, node;
+                    if (!nodes.length) return;
+                    for (j = 0; j < nodes.length; j++) {
+                        node = nodes[j];
+                        if (!node._renderers) {
+                            createRendererForNode(node, rendererClasses);
+                        }
+                        node._contentBox = new kity.Box();
+                        this.fire("beforerender", {
+                            node: node
+                        });
+                    }
+                    // 所有节点渲染器数量是一致的
+                    rendererCount = nodes[0]._renderers.length;
+                    for (i = 0; i < rendererCount; i++) {
+                        // 获取延迟盒子数据
+                        for (j = 0; j < nodes.length; j++) {
+                            if (typeof lastBoxes[j] == "function") {
+                                lastBoxes[j] = lastBoxes[j]();
+                            }
+                            if (!(lastBoxes[j] instanceof kity.Box)) {
+                                lastBoxes[j] = new kity.Box(lastBoxes[j]);
+                            }
+                        }
+                        for (j = 0; j < nodes.length; j++) {
+                            node = nodes[j];
+                            renderer = node._renderers[i];
+                            // 合并盒子
+                            if (lastBoxes[j]) {
+                                node._contentBox = node._contentBox.merge(lastBoxes[j]);
+                                renderer.contentBox = lastBoxes[j];
+                            }
+                            // 判断当前上下文是否应该渲染
+                            if (renderer.shouldRender(node)) {
+                                // 应该渲染,但是渲染图形没创建过,需要创建
+                                if (!renderer.getRenderShape()) {
+                                    renderer.setRenderShape(renderer.create(node));
+                                    if (renderer.bringToBack) {
+                                        node.getRenderContainer().prependShape(renderer.getRenderShape());
+                                    } else {
+                                        node.getRenderContainer().appendShape(renderer.getRenderShape());
+                                    }
+                                }
+                                // 强制让渲染图形显示
+                                renderer.getRenderShape().setVisible(true);
+                                // 更新渲染图形
+                                lastBoxes[j] = renderer.update(renderer.getRenderShape(), node, node._contentBox);
+                            } else if (renderer.getRenderShape()) {
+                                renderer.getRenderShape().setVisible(false);
+                                lastBoxes[j] = null;
+                            }
+                        }
+                    }
+                    for (j = 0; j < nodes.length; j++) {
+                        this.fire("noderender", {
+                            node: nodes[j]
+                        });
+                    }
+                },
+                renderNode: function(node) {
+                    var rendererClasses = this._rendererClasses;
+                    var i, latestBox, renderer;
+                    if (!node._renderers) {
+                        createRendererForNode(node, rendererClasses);
+                    }
+                    this.fire("beforerender", {
+                        node: node
+                    });
+                    node._contentBox = new kity.Box();
+                    node._renderers.forEach(function(renderer) {
+                        // 判断当前上下文是否应该渲染
+                        if (renderer.shouldRender(node)) {
+                            // 应该渲染,但是渲染图形没创建过,需要创建
+                            if (!renderer.getRenderShape()) {
+                                renderer.setRenderShape(renderer.create(node));
+                                if (renderer.bringToBack) {
+                                    node.getRenderContainer().prependShape(renderer.getRenderShape());
+                                } else {
+                                    node.getRenderContainer().appendShape(renderer.getRenderShape());
+                                }
+                            }
+                            // 强制让渲染图形显示
+                            renderer.getRenderShape().setVisible(true);
+                            // 更新渲染图形
+                            latestBox = renderer.update(renderer.getRenderShape(), node, node._contentBox);
+                            if (typeof latestBox == "function") latestBox = latestBox();
+                            // 合并渲染区域
+                            if (latestBox) {
+                                node._contentBox = node._contentBox.merge(latestBox);
+                                renderer.contentBox = latestBox;
+                            }
+                        } else if (renderer.getRenderShape()) {
+                            renderer.getRenderShape().setVisible(false);
+                        }
+                    });
+                    this.fire("noderender", {
+                        node: node
+                    });
+                }
+            };
+        }
+        kity.extendClass(Minder, createMinderExtension());
+        kity.extendClass(MinderNode, {
+            render: function() {
+                if (!this.attached) return;
+                this.getMinder().renderNode(this);
+                return this;
+            },
+            renderTree: function() {
+                if (!this.attached) return;
+                var list = [];
+                this.traverse(function(node) {
+                    list.push(node);
+                });
+                this.getMinder().renderNodeBatch(list);
+                return this;
+            },
+            getRenderer: function(type) {
+                var rs = this._renderers;
+                if (!rs) return null;
+                for (var i = 0; i < rs.length; i++) {
+                    if (rs[i].getType() == type) return rs[i];
+                }
+                return null;
+            },
+            getContentBox: function() {
+                //if (!this._contentBox) this.render();
+                return this.parent && this.parent.isCollapsed() ? new kity.Box() : this._contentBox || new kity.Box();
+            },
+            getRenderBox: function(rendererType, refer) {
+                var renderer = rendererType && this.getRenderer(rendererType);
+                var contentBox = renderer ? renderer.contentBox : this.getContentBox();
+                var ctm = kity.Matrix.getCTM(this.getRenderContainer(), refer || "paper");
+                return ctm.transformBox(contentBox);
+            }
+        });
+        module.exports = Renderer;
+    }
+};
+
+//src/core/select.js
+_p[28] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        Minder.registerInitHook(function() {
+            this._initSelection();
+        });
+        // 选区管理
+        kity.extendClass(Minder, {
+            _initSelection: function() {
+                this._selectedNodes = [];
+            },
+            renderChangedSelection: function(last) {
+                var current = this.getSelectedNodes();
+                var changed = [];
+                current.forEach(function(node) {
+                    if (last.indexOf(node) == -1) {
+                        changed.push(node);
+                    }
+                });
+                last.forEach(function(node) {
+                    if (current.indexOf(node) == -1) {
+                        changed.push(node);
+                    }
+                });
+                if (changed.length) {
+                    this._interactChange();
+                    this.fire("selectionchange");
+                }
+                while (changed.length) {
+                    changed.shift().render();
+                }
+            },
+            getSelectedNodes: function() {
+                //不能克隆返回,会对当前选区操作,从而影响querycommand
+                return this._selectedNodes;
+            },
+            getSelectedNode: function() {
+                return this.getSelectedNodes()[0] || null;
+            },
+            removeAllSelectedNodes: function() {
+                var me = this;
+                var last = this._selectedNodes.splice(0);
+                this._selectedNodes = [];
+                this.renderChangedSelection(last);
+                return this.fire("selectionclear");
+            },
+            removeSelectedNodes: function(nodes) {
+                var me = this;
+                var last = this._selectedNodes.slice(0);
+                nodes = utils.isArray(nodes) ? nodes : [ nodes ];
+                nodes.forEach(function(node) {
+                    var index;
+                    if ((index = me._selectedNodes.indexOf(node)) === -1) return;
+                    me._selectedNodes.splice(index, 1);
+                });
+                this.renderChangedSelection(last);
+                return this;
+            },
+            select: function(nodes, isSingleSelect) {
+                var lastSelect = this.getSelectedNodes().slice(0);
+                if (isSingleSelect) {
+                    this._selectedNodes = [];
+                }
+                var me = this;
+                nodes = utils.isArray(nodes) ? nodes : [ nodes ];
+                nodes.forEach(function(node) {
+                    if (me._selectedNodes.indexOf(node) !== -1) return;
+                    me._selectedNodes.unshift(node);
+                });
+                this.renderChangedSelection(lastSelect);
+                return this;
+            },
+            selectById: function(ids, isSingleSelect) {
+                ids = utils.isArray(ids) ? ids : [ ids ];
+                var nodes = this.getNodesById(ids);
+                return this.select(nodes, isSingleSelect);
+            },
+            //当前选区中的节点在给定的节点范围内的保留选中状态,
+            //没在给定范围的取消选中,给定范围中的但没在当前选中范围的也做选中效果
+            toggleSelect: function(node) {
+                if (utils.isArray(node)) {
+                    node.forEach(this.toggleSelect.bind(this));
+                } else {
+                    if (node.isSelected()) this.removeSelectedNodes(node); else this.select(node);
+                }
+                return this;
+            },
+            isSingleSelect: function() {
+                return this._selectedNodes.length == 1;
+            },
+            getSelectedAncestors: function(includeRoot) {
+                var nodes = this.getSelectedNodes().slice(0), ancestors = [], judge;
+                // 根节点不参与计算
+                var rootIndex = nodes.indexOf(this.getRoot());
+                if (~rootIndex && !includeRoot) {
+                    nodes.splice(rootIndex, 1);
+                }
+                // 判断 nodes 列表中是否存在 judge 的祖先
+                function hasAncestor(nodes, judge) {
+                    for (var i = nodes.length - 1; i >= 0; --i) {
+                        if (nodes[i].isAncestorOf(judge)) return true;
+                    }
+                    return false;
+                }
+                // 按照拓扑排序
+                nodes.sort(function(node1, node2) {
+                    return node1.getLevel() - node2.getLevel();
+                });
+                // 因为是拓扑有序的,所以只需往上查找
+                while (judge = nodes.pop()) {
+                    if (!hasAncestor(nodes, judge)) {
+                        ancestors.push(judge);
+                    }
+                }
+                return ancestors;
+            }
+        });
+        kity.extendClass(MinderNode, {
+            isSelected: function() {
+                var minder = this.getMinder();
+                return minder && minder.getSelectedNodes().indexOf(this) != -1;
+            }
+        });
+    }
+};
+
+//src/core/shortcut.js
+/**
+ * @fileOverview
+ *
+ * 添加快捷键支持
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[29] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var keymap = _p.r(15);
+        var Minder = _p.r(19);
+        var MinderEvent = _p.r(13);
+        /**
+     * 计算包含 meta 键的 keycode
+     *
+     * @param  {String|KeyEvent} unknown
+     */
+        function getMetaKeyCode(unknown) {
+            var CTRL_MASK = 4096;
+            var ALT_MASK = 8192;
+            var SHIFT_MASK = 16384;
+            var metaKeyCode = 0;
+            if (typeof unknown == "string") {
+                // unknown as string
+                unknown.toLowerCase().split(/\+\s*/).forEach(function(name) {
+                    switch (name) {
+                      case "ctrl":
+                      case "cmd":
+                        metaKeyCode |= CTRL_MASK;
+                        break;
+
+                      case "alt":
+                        metaKeyCode |= ALT_MASK;
+                        break;
+
+                      case "shift":
+                        metaKeyCode |= SHIFT_MASK;
+                        break;
+
+                      default:
+                        metaKeyCode |= keymap[name];
+                    }
+                });
+            } else {
+                // unknown as key event
+                if (unknown.ctrlKey || unknown.metaKey) {
+                    metaKeyCode |= CTRL_MASK;
+                }
+                if (unknown.altKey) {
+                    metaKeyCode |= ALT_MASK;
+                }
+                if (unknown.shiftKey) {
+                    metaKeyCode |= SHIFT_MASK;
+                }
+                metaKeyCode |= unknown.keyCode;
+            }
+            return metaKeyCode;
+        }
+        kity.extendClass(MinderEvent, {
+            isShortcutKey: function(keyCombine) {
+                var keyEvent = this.originEvent;
+                if (!keyEvent) return false;
+                return getMetaKeyCode(keyCombine) == getMetaKeyCode(keyEvent);
+            }
+        });
+        Minder.registerInitHook(function() {
+            this._initShortcutKey();
+        });
+        kity.extendClass(Minder, {
+            _initShortcutKey: function() {
+                this._bindShortcutKeys();
+            },
+            _bindShortcutKeys: function() {
+                var map = this._shortcutKeys = {};
+                var has = "hasOwnProperty";
+                this.on("keydown", function(e) {
+                    for (var keys in map) {
+                        if (!map[has](keys)) continue;
+                        if (e.isShortcutKey(keys)) {
+                            var fn = map[keys];
+                            if (fn.__statusCondition && fn.__statusCondition != this.getStatus()) return;
+                            fn();
+                            e.preventDefault();
+                        }
+                    }
+                });
+            },
+            addShortcut: function(keys, fn) {
+                var binds = this._shortcutKeys;
+                keys.split(/\|\s*/).forEach(function(combine) {
+                    var parts = combine.split("::");
+                    var status;
+                    if (parts.length > 1) {
+                        combine = parts[1];
+                        status = parts[0];
+                        fn.__statusCondition = status;
+                    }
+                    binds[combine] = fn;
+                });
+            },
+            addCommandShortcutKeys: function(cmd, keys) {
+                var binds = this._commandShortcutKeys || (this._commandShortcutKeys = {});
+                var obj = {}, km = this;
+                if (keys) {
+                    obj[cmd] = keys;
+                } else {
+                    obj = cmd;
+                }
+                var minder = this;
+                utils.each(obj, function(keys, command) {
+                    binds[command] = keys;
+                    minder.addShortcut(keys, function execCommandByShortcut() {
+                        /**
+                     * 之前判断有问题,由 === 0 改为 !== -1
+                     * @editor Naixor
+                     * @Date 2015-12-2
+                     */
+                        if (minder.queryCommandState(command) !== -1) {
+                            minder.execCommand(command);
+                        }
+                    });
+                });
+            },
+            getCommandShortcutKey: function(cmd) {
+                var binds = this._commandShortcutKeys;
+                return binds && binds[cmd] || null;
+            },
+            /**
+         * @Desc: 添加一个判断是否支持原生Clipboard的变量,用于对ctrl + v和ctrl + c的处理
+         * @Editor: Naixor
+         * @Date: 2015.9.20
+         */
+            supportClipboardEvent: function(window) {
+                return !!window.ClipboardEvent;
+            }(window)
+        });
+    }
+};
+
+//src/core/status.js
+/**
+ * @fileOverview
+ *
+ * 状态切换控制
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[30] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Minder = _p.r(19);
+        var sf = ~window.location.href.indexOf("status");
+        var tf = ~window.location.href.indexOf("trace");
+        Minder.registerInitHook(function() {
+            this._initStatus();
+        });
+        kity.extendClass(Minder, {
+            _initStatus: function() {
+                this._status = "normal";
+                this._rollbackStatus = "normal";
+            },
+            setStatus: function(status, force) {
+                // 在 readonly 模式下,只有 force 为 true 才能切换回来
+                if (this._status == "readonly" && !force) return this;
+                if (status != this._status) {
+                    this._rollbackStatus = this._status;
+                    this._status = status;
+                    this.fire("statuschange", {
+                        lastStatus: this._rollbackStatus,
+                        currentStatus: this._status
+                    });
+                    if (sf) {
+                        /* global console: true */
+                        console.log(window.event.type, this._rollbackStatus, "->", this._status);
+                        if (tf) {
+                            console.trace();
+                        }
+                    }
+                }
+                return this;
+            },
+            rollbackStatus: function() {
+                this.setStatus(this._rollbackStatus);
+            },
+            getRollbackStatus: function() {
+                return this._rollbackStatus;
+            },
+            getStatus: function() {
+                return this._status;
+            }
+        });
+    }
+};
+
+//src/core/template.js
+_p[31] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var Command = _p.r(9);
+        var MinderNode = _p.r(21);
+        var Module = _p.r(20);
+        var _templates = {};
+        function register(name, supports) {
+            _templates[name] = supports;
+        }
+        exports.register = register;
+        utils.extend(Minder, {
+            getTemplateList: function() {
+                return _templates;
+            }
+        });
+        kity.extendClass(Minder, function() {
+            var originGetTheme = Minder.prototype.getTheme;
+            return {
+                useTemplate: function(name, duration) {
+                    this.setTemplate(name);
+                    this.refresh(duration || 800);
+                },
+                getTemplate: function() {
+                    return this._template || "default";
+                },
+                setTemplate: function(name) {
+                    this._template = name || null;
+                },
+                getTemplateSupport: function(method) {
+                    var supports = _templates[this.getTemplate()];
+                    return supports && supports[method];
+                },
+                getTheme: function(node) {
+                    var support = this.getTemplateSupport("getTheme") || originGetTheme;
+                    return support.call(this, node);
+                }
+            };
+        }());
+        kity.extendClass(MinderNode, function() {
+            var originGetLayout = MinderNode.prototype.getLayout;
+            var originGetConnect = MinderNode.prototype.getConnect;
+            return {
+                getLayout: function() {
+                    var support = this.getMinder().getTemplateSupport("getLayout") || originGetLayout;
+                    return support.call(this, this);
+                },
+                getConnect: function() {
+                    var support = this.getMinder().getTemplateSupport("getConnect") || originGetConnect;
+                    return support.call(this, this);
+                }
+            };
+        }());
+        Module.register("TemplateModule", {
+            /**
+         * @command Template
+         * @description 设置当前脑图的模板
+         * @param {string} name 模板名称
+         *    允许使用的模板可以使用 `kityminder.Minder.getTemplateList()` 查询
+         * @state
+         *   0: 始终可用
+         * @return 返回当前的模板名称
+         */
+            commands: {
+                template: kity.createClass("TemplateCommand", {
+                    base: Command,
+                    execute: function(minder, name) {
+                        minder.useTemplate(name);
+                        minder.execCommand("camera");
+                    },
+                    queryValue: function(minder) {
+                        return minder.getTemplate() || "default";
+                    }
+                })
+            }
+        });
+    }
+};
+
+//src/core/theme.js
+_p[32] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Module = _p.r(20);
+        var Command = _p.r(9);
+        var cssLikeValueMatcher = {
+            left: function(value) {
+                return 3 in value && value[3] || 1 in value && value[1] || value[0];
+            },
+            right: function(value) {
+                return 1 in value && value[1] || value[0];
+            },
+            top: function(value) {
+                return value[0];
+            },
+            bottom: function(value) {
+                return 2 in value && value[2] || value[0];
+            }
+        };
+        var _themes = {};
+        /**
+     * 注册一个主题
+     *
+     * @param  {String} name  主题的名称
+     * @param  {Plain} theme 主题的样式描述
+     *
+     * @example
+     *     Minder.registerTheme('default', {
+     *         'root-color': 'red',
+     *         'root-stroke': 'none',
+     *         'root-padding': [10, 20]
+     *     });
+     */
+        function register(name, theme) {
+            _themes[name] = theme;
+        }
+        exports.register = register;
+        utils.extend(Minder, {
+            getThemeList: function() {
+                return _themes;
+            }
+        });
+        kity.extendClass(Minder, {
+            /**
+         * 切换脑图实例上的主题
+         * @param  {String} name 要使用的主题的名称
+         */
+            useTheme: function(name) {
+                this.setTheme(name);
+                this.refresh(800);
+                return true;
+            },
+            setTheme: function(name) {
+                if (name && !_themes[name]) throw new Error("Theme " + name + " not exists!");
+                var lastTheme = this._theme;
+                this._theme = name || null;
+                var container = this.getRenderTarget();
+                if (container) {
+                    container.classList.remove("km-theme-" + lastTheme);
+                    if (name) {
+                        container.classList.add("km-theme-" + name);
+                    }
+                    container.style.background = this.getStyle("background");
+                }
+                this.fire("themechange", {
+                    theme: name
+                });
+                return this;
+            },
+            /**
+         * 获取脑图实例上的当前主题
+         * @return {[type]} [description]
+         */
+            getTheme: function(node) {
+                return this._theme || this.getOption("defaultTheme") || "fresh-blue";
+            },
+            getThemeItems: function(node) {
+                var theme = this.getTheme(node);
+                return _themes[this.getTheme(node)];
+            },
+            /**
+         * 获得脑图实例上的样式
+         * @param  {String} item 样式名称
+         */
+            getStyle: function(item, node) {
+                var items = this.getThemeItems(node);
+                var segment, dir, selector, value, matcher;
+                if (item in items) return items[item];
+                // 尝试匹配 CSS 数组形式的值
+                // 比如 item 为 'pading-left'
+                // theme 里有 {'padding': [10, 20]} 的定义,则可以返回 20
+                segment = item.split("-");
+                if (segment.length < 2) return null;
+                dir = segment.pop();
+                item = segment.join("-");
+                if (item in items) {
+                    value = items[item];
+                    if (utils.isArray(value) && (matcher = cssLikeValueMatcher[dir])) {
+                        return matcher(value);
+                    }
+                    if (!isNaN(value)) return value;
+                }
+                return null;
+            },
+            /**
+         * 获取指定节点的样式
+         * @param  {String} name 样式名称,可以不加节点类型的前缀
+         */
+            getNodeStyle: function(node, name) {
+                var value = this.getStyle(node.getType() + "-" + name, node);
+                return value !== null ? value : this.getStyle(name, node);
+            }
+        });
+        kity.extendClass(MinderNode, {
+            getStyle: function(name) {
+                return this.getMinder().getNodeStyle(this, name);
+            }
+        });
+        Module.register("Theme", {
+            defaultOptions: {
+                defaultTheme: "fresh-blue"
+            },
+            commands: {
+                /**
+             * @command Theme
+             * @description 设置当前脑图的主题
+             * @param {string} name 主题名称
+             *    允许使用的主题可以使用 `kityminder.Minder.getThemeList()` 查询
+             * @state
+             *   0: 始终可用
+             * @return 返回当前的主题名称
+             */
+                theme: kity.createClass("ThemeCommand", {
+                    base: Command,
+                    execute: function(km, name) {
+                        return km.useTheme(name);
+                    },
+                    queryValue: function(km) {
+                        return km.getTheme() || "default";
+                    }
+                })
+            }
+        });
+        Minder.registerInitHook(function() {
+            this.setTheme();
+        });
+    }
+};
+
+//src/core/utils.js
+_p[33] = {
+    value: function(require, exports) {
+        var kity = _p.r(17);
+        var uuidMap = {};
+        exports.extend = kity.Utils.extend.bind(kity.Utils);
+        exports.each = kity.Utils.each.bind(kity.Utils);
+        exports.uuid = function(group) {
+            uuidMap[group] = uuidMap[group] ? uuidMap[group] + 1 : 1;
+            return group + uuidMap[group];
+        };
+        exports.guid = function() {
+            return (+new Date() * 1e6 + Math.floor(Math.random() * 1e6)).toString(36);
+        };
+        exports.trim = function(str) {
+            return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, "");
+        };
+        exports.keys = function(plain) {
+            var keys = [];
+            for (var key in plain) {
+                if (plain.hasOwnProperty(key)) {
+                    keys.push(key);
+                }
+            }
+            return keys;
+        };
+        exports.clone = function(source) {
+            return JSON.parse(JSON.stringify(source));
+        };
+        exports.comparePlainObject = function(a, b) {
+            return JSON.stringify(a) == JSON.stringify(b);
+        };
+        exports.encodeHtml = function(str, reg) {
+            return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp);)?/g, function(a, b) {
+                if (b) {
+                    return a;
+                } else {
+                    return {
+                        "<": "&lt;",
+                        "&": "&amp;",
+                        '"': "&quot;",
+                        ">": "&gt;",
+                        "'": "&#39;"
+                    }[a];
+                }
+            }) : "";
+        };
+        exports.clearWhiteSpace = function(str) {
+            return str.replace(/[\u200b\t\r\n]/g, "");
+        };
+        exports.each([ "String", "Function", "Array", "Number", "RegExp", "Object" ], function(v) {
+            var toString = Object.prototype.toString;
+            exports["is" + v] = function(obj) {
+                return toString.apply(obj) == "[object " + v + "]";
+            };
+        });
+    }
+};
+
+//src/expose-kityminder.js
+_p[34] = {
+    value: function(require, exports, module) {
+        module.exports = window.kityminder = _p.r(35);
+    }
+};
+
+//src/kityminder.js
+/**
+ * @fileOverview
+ *
+ * 默认导出(全部模块)
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[35] = {
+    value: function(require, exports, module) {
+        var kityminder = {
+            version: _p.r(19).version
+        };
+        // 核心导出,大写的部分导出类,小写的部分简单 require 一下
+        // 这里顺序是有讲究的,调整前先弄清楚依赖关系。
+        _p.r(33);
+        kityminder.Minder = _p.r(19);
+        kityminder.Command = _p.r(9);
+        kityminder.Node = _p.r(21);
+        _p.r(22);
+        _p.r(8);
+        kityminder.Event = _p.r(13);
+        kityminder.data = _p.r(12);
+        _p.r(10);
+        kityminder.KeyMap = _p.r(15);
+        _p.r(29);
+        _p.r(30);
+        _p.r(23);
+        _p.r(28);
+        _p.r(14);
+        _p.r(16);
+        kityminder.Module = _p.r(20);
+        _p.r(26);
+        kityminder.Render = _p.r(27);
+        kityminder.Connect = _p.r(11);
+        kityminder.Layout = _p.r(18);
+        kityminder.Theme = _p.r(32);
+        kityminder.Template = _p.r(31);
+        kityminder.Promise = _p.r(25);
+        _p.r(7);
+        _p.r(24);
+        // 模块依赖
+        _p.r(42);
+        _p.r(43);
+        _p.r(44);
+        _p.r(45);
+        _p.r(46);
+        _p.r(47);
+        _p.r(48);
+        _p.r(50);
+        _p.r(49);
+        _p.r(51);
+        _p.r(52);
+        _p.r(53);
+        _p.r(54);
+        _p.r(55);
+        _p.r(56);
+        _p.r(57);
+        _p.r(58);
+        _p.r(59);
+        _p.r(60);
+        _p.r(61);
+        _p.r(62);
+        _p.r(63);
+        _p.r(64);
+        _p.r(68);
+        _p.r(65);
+        _p.r(67);
+        _p.r(66);
+        _p.r(40);
+        _p.r(36);
+        _p.r(37);
+        _p.r(38);
+        _p.r(39);
+        _p.r(41);
+        _p.r(75);
+        _p.r(78);
+        _p.r(77);
+        _p.r(76);
+        _p.r(78);
+        _p.r(80);
+        _p.r(79);
+        _p.r(0);
+        _p.r(1);
+        _p.r(2);
+        _p.r(3);
+        _p.r(4);
+        _p.r(5);
+        _p.r(6);
+        _p.r(69);
+        _p.r(73);
+        _p.r(70);
+        _p.r(72);
+        _p.r(71);
+        _p.r(74);
+        module.exports = kityminder;
+    }
+};
+
+//src/layout/btree.js
+_p[36] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Layout = _p.r(18);
+        [ "left", "right", "top", "bottom" ].forEach(registerLayoutForDirection);
+        function registerLayoutForDirection(name) {
+            var axis = name == "left" || name == "right" ? "x" : "y";
+            var dir = name == "left" || name == "top" ? -1 : 1;
+            var oppsite = {
+                left: "right",
+                right: "left",
+                top: "bottom",
+                bottom: "top",
+                x: "y",
+                y: "x"
+            };
+            function getOrderHint(node) {
+                var hint = [];
+                var box = node.getLayoutBox();
+                var offset = 5;
+                if (axis == "x") {
+                    hint.push({
+                        type: "up",
+                        node: node,
+                        area: new kity.Box({
+                            x: box.x,
+                            y: box.top - node.getStyle("margin-top") - offset,
+                            width: box.width,
+                            height: node.getStyle("margin-top")
+                        }),
+                        path: [ "M", box.x, box.top - offset, "L", box.right, box.top - offset ]
+                    });
+                    hint.push({
+                        type: "down",
+                        node: node,
+                        area: new kity.Box({
+                            x: box.x,
+                            y: box.bottom + offset,
+                            width: box.width,
+                            height: node.getStyle("margin-bottom")
+                        }),
+                        path: [ "M", box.x, box.bottom + offset, "L", box.right, box.bottom + offset ]
+                    });
+                } else {
+                    hint.push({
+                        type: "up",
+                        node: node,
+                        area: new kity.Box({
+                            x: box.left - node.getStyle("margin-left") - offset,
+                            y: box.top,
+                            width: node.getStyle("margin-left"),
+                            height: box.height
+                        }),
+                        path: [ "M", box.left - offset, box.top, "L", box.left - offset, box.bottom ]
+                    });
+                    hint.push({
+                        type: "down",
+                        node: node,
+                        area: new kity.Box({
+                            x: box.right + offset,
+                            y: box.top,
+                            width: node.getStyle("margin-right"),
+                            height: box.height
+                        }),
+                        path: [ "M", box.right + offset, box.top, "L", box.right + offset, box.bottom ]
+                    });
+                }
+                return hint;
+            }
+            Layout.register(name, kity.createClass({
+                base: Layout,
+                doLayout: function(parent, children) {
+                    var pbox = parent.getContentBox();
+                    if (axis == "x") {
+                        parent.setVertexOut(new kity.Point(pbox[name], pbox.cy));
+                        parent.setLayoutVectorOut(new kity.Vector(dir, 0));
+                    } else {
+                        parent.setVertexOut(new kity.Point(pbox.cx, pbox[name]));
+                        parent.setLayoutVectorOut(new kity.Vector(0, dir));
+                    }
+                    if (!children.length) {
+                        return false;
+                    }
+                    children.forEach(function(child) {
+                        var cbox = child.getContentBox();
+                        child.setLayoutTransform(new kity.Matrix());
+                        if (axis == "x") {
+                            child.setVertexIn(new kity.Point(cbox[oppsite[name]], cbox.cy));
+                            child.setLayoutVectorIn(new kity.Vector(dir, 0));
+                        } else {
+                            child.setVertexIn(new kity.Point(cbox.cx, cbox[oppsite[name]]));
+                            child.setLayoutVectorIn(new kity.Vector(0, dir));
+                        }
+                    });
+                    this.align(children, oppsite[name]);
+                    this.stack(children, oppsite[axis]);
+                    var bbox = this.getBranchBox(children);
+                    var xAdjust = 0, yAdjust = 0;
+                    if (axis == "x") {
+                        xAdjust = pbox[name];
+                        xAdjust += dir * parent.getStyle("margin-" + name);
+                        xAdjust += dir * children[0].getStyle("margin-" + oppsite[name]);
+                        yAdjust = pbox.bottom;
+                        yAdjust -= pbox.height / 2;
+                        yAdjust -= bbox.height / 2;
+                        yAdjust -= bbox.y;
+                    } else {
+                        xAdjust = pbox.right;
+                        xAdjust -= pbox.width / 2;
+                        xAdjust -= bbox.width / 2;
+                        xAdjust -= bbox.x;
+                        yAdjust = pbox[name];
+                        yAdjust += dir * parent.getStyle("margin-" + name);
+                        yAdjust += dir * children[0].getStyle("margin-" + oppsite[name]);
+                    }
+                    this.move(children, xAdjust, yAdjust);
+                },
+                getOrderHint: getOrderHint
+            }));
+        }
+    }
+};
+
+//src/layout/filetree.js
+_p[37] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Layout = _p.r(18);
+        [ -1, 1 ].forEach(registerLayoutForDir);
+        function registerLayoutForDir(dir) {
+            var name = "filetree-" + (dir > 0 ? "down" : "up");
+            Layout.register(name, kity.createClass({
+                base: Layout,
+                doLayout: function(parent, children, round) {
+                    var pBox = parent.getContentBox();
+                    var indent = 20;
+                    parent.setVertexOut(new kity.Point(pBox.left + indent, dir > 0 ? pBox.bottom : pBox.top));
+                    parent.setLayoutVectorOut(new kity.Vector(0, dir));
+                    if (!children.length) return;
+                    children.forEach(function(child) {
+                        var cbox = child.getContentBox();
+                        child.setLayoutTransform(new kity.Matrix());
+                        child.setVertexIn(new kity.Point(cbox.left, cbox.cy));
+                        child.setLayoutVectorIn(new kity.Vector(1, 0));
+                    });
+                    this.align(children, "left");
+                    this.stack(children, "y");
+                    var xAdjust = 0;
+                    xAdjust += pBox.left;
+                    xAdjust += indent;
+                    xAdjust += children[0].getStyle("margin-left");
+                    var yAdjust = 0;
+                    if (dir > 0) {
+                        yAdjust += pBox.bottom;
+                        yAdjust += parent.getStyle("margin-bottom");
+                        yAdjust += children[0].getStyle("margin-top");
+                    } else {
+                        yAdjust -= this.getTreeBox(children).bottom;
+                        yAdjust += pBox.top;
+                        yAdjust -= parent.getStyle("margin-top");
+                        yAdjust -= children[0].getStyle("margin-bottom");
+                    }
+                    this.move(children, xAdjust, yAdjust);
+                },
+                getOrderHint: function(node) {
+                    var hint = [];
+                    var box = node.getLayoutBox();
+                    var offset = node.getLevel() > 1 ? 3 : 5;
+                    hint.push({
+                        type: "up",
+                        node: node,
+                        area: new kity.Box({
+                            x: box.x,
+                            y: box.top - node.getStyle("margin-top") - offset,
+                            width: box.width,
+                            height: node.getStyle("margin-top")
+                        }),
+                        path: [ "M", box.x, box.top - offset, "L", box.right, box.top - offset ]
+                    });
+                    hint.push({
+                        type: "down",
+                        node: node,
+                        area: new kity.Box({
+                            x: box.x,
+                            y: box.bottom + offset,
+                            width: box.width,
+                            height: node.getStyle("margin-bottom")
+                        }),
+                        path: [ "M", box.x, box.bottom + offset, "L", box.right, box.bottom + offset ]
+                    });
+                    return hint;
+                }
+            }));
+        }
+    }
+};
+
+//src/layout/fish-bone-master.js
+/**
+ * @fileOverview
+ *
+ * 鱼骨图主骨架布局
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[38] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Layout = _p.r(18);
+        Layout.register("fish-bone-master", kity.createClass("FishBoneMasterLayout", {
+            base: Layout,
+            doLayout: function(parent, children, round) {
+                var upPart = [], downPart = [];
+                var child = children[0];
+                var pBox = parent.getContentBox();
+                parent.setVertexOut(new kity.Point(pBox.right, pBox.cy));
+                parent.setLayoutVectorOut(new kity.Vector(1, 0));
+                if (!child) return;
+                var cBox = child.getContentBox();
+                var pMarginRight = parent.getStyle("margin-right");
+                var cMarginLeft = child.getStyle("margin-left");
+                var cMarginTop = child.getStyle("margin-top");
+                var cMarginBottom = child.getStyle("margin-bottom");
+                children.forEach(function(child, index) {
+                    child.setLayoutTransform(new kity.Matrix());
+                    var cBox = child.getContentBox();
+                    if (index % 2) {
+                        downPart.push(child);
+                        child.setVertexIn(new kity.Point(cBox.left, cBox.top));
+                        child.setLayoutVectorIn(new kity.Vector(1, 1));
+                    } else {
+                        upPart.push(child);
+                        child.setVertexIn(new kity.Point(cBox.left, cBox.bottom));
+                        child.setLayoutVectorIn(new kity.Vector(1, -1));
+                    }
+                });
+                this.stack(upPart, "x");
+                this.stack(downPart, "x");
+                this.align(upPart, "bottom");
+                this.align(downPart, "top");
+                var xAdjust = pBox.right + pMarginRight + cMarginLeft;
+                var yAdjustUp = pBox.cy - cMarginBottom - parent.getStyle("margin-top");
+                var yAdjustDown = pBox.cy + cMarginTop + parent.getStyle("margin-bottom");
+                this.move(upPart, xAdjust, yAdjustUp);
+                this.move(downPart, xAdjust + cMarginLeft, yAdjustDown);
+            }
+        }));
+    }
+};
+
+//src/layout/fish-bone-slave.js
+/**
+ * @fileOverview
+ *
+ *
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[39] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Layout = _p.r(18);
+        Layout.register("fish-bone-slave", kity.createClass("FishBoneSlaveLayout", {
+            base: Layout,
+            doLayout: function(parent, children, round) {
+                var layout = this;
+                var abs = Math.abs;
+                var GOLD_CUT = 1 - .618;
+                var pBox = parent.getContentBox();
+                var vi = parent.getLayoutVectorIn();
+                parent.setLayoutVectorOut(vi);
+                var goldX = pBox.left + pBox.width * GOLD_CUT;
+                var pout = new kity.Point(goldX, vi.y > 0 ? pBox.bottom : pBox.top);
+                parent.setVertexOut(pout);
+                var child = children[0];
+                if (!child) return;
+                var cBox = child.getContentBox();
+                children.forEach(function(child, index) {
+                    child.setLayoutTransform(new kity.Matrix());
+                    child.setLayoutVectorIn(new kity.Vector(1, 0));
+                    child.setVertexIn(new kity.Point(cBox.left, cBox.cy));
+                });
+                this.stack(children, "y");
+                this.align(children, "left");
+                var xAdjust = 0, yAdjust = 0;
+                xAdjust += pout.x;
+                if (parent.getLayoutVectorOut().y < 0) {
+                    yAdjust -= this.getTreeBox(children).bottom;
+                    yAdjust += parent.getContentBox().top;
+                    yAdjust -= parent.getStyle("margin-top");
+                    yAdjust -= child.getStyle("margin-bottom");
+                } else {
+                    yAdjust += parent.getContentBox().bottom;
+                    yAdjust += parent.getStyle("margin-bottom");
+                    yAdjust += child.getStyle("margin-top");
+                }
+                this.move(children, xAdjust, yAdjust);
+                if (round == 2) {
+                    children.forEach(function(child) {
+                        var m = child.getLayoutTransform();
+                        var cbox = child.getContentBox();
+                        var pin = m.transformPoint(new kity.Point(cbox.left, 0));
+                        layout.move([ child ], abs(pin.y - pout.y), 0);
+                    });
+                }
+            }
+        }));
+    }
+};
+
+//src/layout/mind.js
+_p[40] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Layout = _p.r(18);
+        var Minder = _p.r(19);
+        Layout.register("mind", kity.createClass({
+            base: Layout,
+            doLayout: function(node, children) {
+                var layout = this;
+                var half = Math.ceil(node.children.length / 2);
+                var right = [];
+                var left = [];
+                children.forEach(function(child) {
+                    if (child.getIndex() < half) right.push(child); else left.push(child);
+                });
+                var leftLayout = Minder.getLayoutInstance("left");
+                var rightLayout = Minder.getLayoutInstance("right");
+                leftLayout.doLayout(node, left);
+                rightLayout.doLayout(node, right);
+                var box = node.getContentBox();
+                node.setVertexOut(new kity.Point(box.cx, box.cy));
+                node.setLayoutVectorOut(new kity.Vector(0, 0));
+            },
+            getOrderHint: function(node) {
+                var hint = [];
+                var box = node.getLayoutBox();
+                var offset = 5;
+                hint.push({
+                    type: "up",
+                    node: node,
+                    area: new kity.Box({
+                        x: box.x,
+                        y: box.top - node.getStyle("margin-top") - offset,
+                        width: box.width,
+                        height: node.getStyle("margin-top")
+                    }),
+                    path: [ "M", box.x, box.top - offset, "L", box.right, box.top - offset ]
+                });
+                hint.push({
+                    type: "down",
+                    node: node,
+                    area: new kity.Box({
+                        x: box.x,
+                        y: box.bottom + offset,
+                        width: box.width,
+                        height: node.getStyle("margin-bottom")
+                    }),
+                    path: [ "M", box.x, box.bottom + offset, "L", box.right, box.bottom + offset ]
+                });
+                return hint;
+            }
+        }));
+    }
+};
+
+//src/layout/tianpan.js
+/**
+ * @fileOverview
+ *
+ * 天盘模板
+ *
+ * @author: along
+ * @copyright: bpd729@163.com, 2015
+ */
+_p[41] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Layout = _p.r(18);
+        var Minder = _p.r(19);
+        Layout.register("tianpan", kity.createClass({
+            base: Layout,
+            doLayout: function(parent, children) {
+                if (children.length == 0) return;
+                var layout = this;
+                var pbox = parent.getContentBox();
+                var x, y, box;
+                var _theta = 5;
+                var _r = Math.max(pbox.width, 50);
+                children.forEach(function(child, index) {
+                    child.setLayoutTransform(new kity.Matrix());
+                    box = layout.getTreeBox(child);
+                    _r = Math.max(Math.max(box.width, box.height), _r);
+                });
+                _r = _r / 1.5 / Math.PI;
+                children.forEach(function(child, index) {
+                    x = _r * (Math.cos(_theta) + Math.sin(_theta) * _theta);
+                    y = _r * (Math.sin(_theta) - Math.cos(_theta) * _theta);
+                    _theta += .9 - index * .02;
+                    child.setLayoutVectorIn(new kity.Vector(1, 0));
+                    child.setVertexIn(new kity.Point(pbox.cx, pbox.cy));
+                    child.setLayoutTransform(new kity.Matrix());
+                    layout.move([ child ], x, y);
+                });
+            },
+            getOrderHint: function(node) {
+                var hint = [];
+                var box = node.getLayoutBox();
+                var offset = 5;
+                hint.push({
+                    type: "up",
+                    node: node,
+                    area: {
+                        x: box.x,
+                        y: box.top - node.getStyle("margin-top") - offset,
+                        width: box.width,
+                        height: node.getStyle("margin-top")
+                    },
+                    path: [ "M", box.x, box.top - offset, "L", box.right, box.top - offset ]
+                });
+                hint.push({
+                    type: "down",
+                    node: node,
+                    area: {
+                        x: box.x,
+                        y: box.bottom + offset,
+                        width: box.width,
+                        height: node.getStyle("margin-bottom")
+                    },
+                    path: [ "M", box.x, box.bottom + offset, "L", box.right, box.bottom + offset ]
+                });
+                return hint;
+            }
+        }));
+    }
+};
+
+//src/module/arrange.js
+_p[42] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        kity.extendClass(MinderNode, {
+            arrange: function(index) {
+                var parent = this.parent;
+                if (!parent) return;
+                var sibling = parent.children;
+                if (index < 0 || index >= sibling.length) return;
+                sibling.splice(this.getIndex(), 1);
+                sibling.splice(index, 0, this);
+                return this;
+            }
+        });
+        function asc(nodeA, nodeB) {
+            return nodeA.getIndex() - nodeB.getIndex();
+        }
+        function desc(nodeA, nodeB) {
+            return -asc(nodeA, nodeB);
+        }
+        function canArrange(km) {
+            var selected = km.getSelectedNode();
+            return selected && selected.parent && selected.parent.children.length > 1;
+        }
+        /**
+     * @command ArrangeUp
+     * @description 向上调整选中节点的位置
+     * @shortcut Alt + Up
+     * @state
+     *    0: 当前选中了具有相同父亲的节点
+     *   -1: 其它情况
+     */
+        var ArrangeUpCommand = kity.createClass("ArrangeUpCommand", {
+            base: Command,
+            execute: function(km) {
+                var nodes = km.getSelectedNodes();
+                nodes.sort(asc);
+                var lastIndexes = nodes.map(function(node) {
+                    return node.getIndex();
+                });
+                nodes.forEach(function(node, index) {
+                    node.arrange(lastIndexes[index] - 1);
+                });
+                km.layout(300);
+            },
+            queryState: function(km) {
+                var selected = km.getSelectedNode();
+                return selected ? 0 : -1;
+            }
+        });
+        /**
+     * @command ArrangeDown
+     * @description 向下调整选中节点的位置
+     * @shortcut Alt + Down
+     * @state
+     *    0: 当前选中了具有相同父亲的节点
+     *   -1: 其它情况
+     */
+        var ArrangeDownCommand = kity.createClass("ArrangeUpCommand", {
+            base: Command,
+            execute: function(km) {
+                var nodes = km.getSelectedNodes();
+                nodes.sort(desc);
+                var lastIndexes = nodes.map(function(node) {
+                    return node.getIndex();
+                });
+                nodes.forEach(function(node, index) {
+                    node.arrange(lastIndexes[index] + 1);
+                });
+                km.layout(300);
+            },
+            queryState: function(km) {
+                var selected = km.getSelectedNode();
+                return selected ? 0 : -1;
+            }
+        });
+        /**
+     * @command Arrange
+     * @description 调整选中节点的位置
+     * @param {number} index 调整后节点的新位置
+     * @state
+     *    0: 当前选中了具有相同父亲的节点
+     *   -1: 其它情况
+     */
+        var ArrangeCommand = kity.createClass("ArrangeCommand", {
+            base: Command,
+            execute: function(km, index) {
+                var nodes = km.getSelectedNodes().slice();
+                if (!nodes.length) return;
+                var ancestor = MinderNode.getCommonAncestor(nodes);
+                if (ancestor != nodes[0].parent) return;
+                var indexed = nodes.map(function(node) {
+                    return {
+                        index: node.getIndex(),
+                        node: node
+                    };
+                });
+                var asc = Math.min.apply(Math, indexed.map(function(one) {
+                    return one.index;
+                })) >= index;
+                indexed.sort(function(a, b) {
+                    return asc ? b.index - a.index : a.index - b.index;
+                });
+                indexed.forEach(function(one) {
+                    one.node.arrange(index);
+                });
+                km.layout(300);
+            },
+            queryState: function(km) {
+                var selected = km.getSelectedNode();
+                return selected ? 0 : -1;
+            }
+        });
+        Module.register("ArrangeModule", {
+            commands: {
+                arrangeup: ArrangeUpCommand,
+                arrangedown: ArrangeDownCommand,
+                arrange: ArrangeCommand
+            },
+            contextmenu: [ {
+                command: "arrangeup"
+            }, {
+                command: "arrangedown"
+            }, {
+                divider: true
+            } ],
+            commandShortcutKeys: {
+                arrangeup: "normal::alt+Up",
+                arrangedown: "normal::alt+Down"
+            }
+        });
+    }
+};
+
+//src/module/basestyle.js
+_p[43] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var TextRenderer = _p.r(61);
+        Module.register("basestylemodule", function() {
+            var km = this;
+            function getNodeDataOrStyle(node, name) {
+                return node.getData(name) || node.getStyle(name);
+            }
+            TextRenderer.registerStyleHook(function(node, textGroup) {
+                var fontWeight = getNodeDataOrStyle(node, "font-weight");
+                var fontStyle = getNodeDataOrStyle(node, "font-style");
+                var styleHash = [ fontWeight, fontStyle ].join("/");
+                textGroup.eachItem(function(index, item) {
+                    item.setFont({
+                        weight: fontWeight,
+                        style: fontStyle
+                    });
+                });
+            });
+            return {
+                commands: {
+                    /**
+                 * @command Bold
+                 * @description 加粗选中的节点
+                 * @shortcut Ctrl + B
+                 * @state
+                 *   0: 当前有选中的节点
+                 *  -1: 当前没有选中的节点
+                 *   1: 当前已选中的节点已加粗
+                 */
+                    bold: kity.createClass("boldCommand", {
+                        base: Command,
+                        execute: function(km) {
+                            var nodes = km.getSelectedNodes();
+                            if (this.queryState("bold") == 1) {
+                                nodes.forEach(function(n) {
+                                    n.setData("font-weight").render();
+                                });
+                            } else {
+                                nodes.forEach(function(n) {
+                                    n.setData("font-weight", "bold").render();
+                                });
+                            }
+                            km.layout();
+                        },
+                        queryState: function() {
+                            var nodes = km.getSelectedNodes(), result = 0;
+                            if (nodes.length === 0) {
+                                return -1;
+                            }
+                            nodes.forEach(function(n) {
+                                if (n && n.getData("font-weight")) {
+                                    result = 1;
+                                    return false;
+                                }
+                            });
+                            return result;
+                        }
+                    }),
+                    /**
+                 * @command Italic
+                 * @description 加斜选中的节点
+                 * @shortcut Ctrl + I
+                 * @state
+                 *   0: 当前有选中的节点
+                 *  -1: 当前没有选中的节点
+                 *   1: 当前已选中的节点已加斜
+                 */
+                    italic: kity.createClass("italicCommand", {
+                        base: Command,
+                        execute: function(km) {
+                            var nodes = km.getSelectedNodes();
+                            if (this.queryState("italic") == 1) {
+                                nodes.forEach(function(n) {
+                                    n.setData("font-style").render();
+                                });
+                            } else {
+                                nodes.forEach(function(n) {
+                                    n.setData("font-style", "italic").render();
+                                });
+                            }
+                            km.layout();
+                        },
+                        queryState: function() {
+                            var nodes = km.getSelectedNodes(), result = 0;
+                            if (nodes.length === 0) {
+                                return -1;
+                            }
+                            nodes.forEach(function(n) {
+                                if (n && n.getData("font-style")) {
+                                    result = 1;
+                                    return false;
+                                }
+                            });
+                            return result;
+                        }
+                    })
+                },
+                commandShortcutKeys: {
+                    bold: "ctrl+b",
+                    //bold
+                    italic: "ctrl+i"
+                }
+            };
+        });
+    }
+};
+
+//src/module/clipboard.js
+_p[44] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        Module.register("ClipboardModule", function() {
+            var km = this, _clipboardNodes = [], _selectedNodes = [];
+            function appendChildNode(parent, child) {
+                _selectedNodes.push(child);
+                km.appendNode(child, parent);
+                child.render();
+                child.setLayoutOffset(null);
+                var children = child.children.map(function(node) {
+                    return node.clone();
+                });
+                /*
+            * fixed bug: Modified on 2015.08.05
+            * 原因:粘贴递归 append 时没有清空原来父节点的子节点,而父节点被复制的时候,是连同子节点一起复制过来的
+            * 解决办法:增加了下面这一行代码
+            * by: @zhangbobell zhangbobell@163.com
+            */
+                child.clearChildren();
+                for (var i = 0, ci; ci = children[i]; i++) {
+                    appendChildNode(child, ci);
+                }
+            }
+            function sendToClipboard(nodes) {
+                if (!nodes.length) return;
+                nodes.sort(function(a, b) {
+                    return a.getIndex() - b.getIndex();
+                });
+                _clipboardNodes = nodes.map(function(node) {
+                    return node.clone();
+                });
+            }
+            /**
+         * @command Copy
+         * @description 复制当前选中的节点
+         * @shortcut Ctrl + C
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+            var CopyCommand = kity.createClass("CopyCommand", {
+                base: Command,
+                execute: function(km) {
+                    sendToClipboard(km.getSelectedAncestors(true));
+                    this.setContentChanged(false);
+                }
+            });
+            /**
+         * @command Cut
+         * @description 剪切当前选中的节点
+         * @shortcut Ctrl + X
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+            var CutCommand = kity.createClass("CutCommand", {
+                base: Command,
+                execute: function(km) {
+                    var ancestors = km.getSelectedAncestors();
+                    if (ancestors.length === 0) return;
+                    sendToClipboard(ancestors);
+                    km.select(MinderNode.getCommonAncestor(ancestors), true);
+                    ancestors.slice().forEach(function(node) {
+                        km.removeNode(node);
+                    });
+                    km.layout(300);
+                }
+            });
+            /**
+         * @command Paste
+         * @description 粘贴已复制的节点到每一个当前选中的节点上
+         * @shortcut Ctrl + V
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+            var PasteCommand = kity.createClass("PasteCommand", {
+                base: Command,
+                execute: function(km) {
+                    if (_clipboardNodes.length) {
+                        var nodes = km.getSelectedNodes();
+                        if (!nodes.length) return;
+                        for (var i = 0, ni; ni = _clipboardNodes[i]; i++) {
+                            for (var j = 0, node; node = nodes[j]; j++) {
+                                appendChildNode(node, ni.clone());
+                            }
+                        }
+                        km.select(_selectedNodes, true);
+                        _selectedNodes = [];
+                        km.layout(300);
+                    }
+                },
+                queryState: function(km) {
+                    return km.getSelectedNode() ? 0 : -1;
+                }
+            });
+            /**
+         * @Desc: 若支持原生clipboadr事件则基于原生扩展,否则使用km的基础事件只处理节点的粘贴复制
+         * @Editor: Naixor
+         * @Date: 2015.9.20
+         */
+            if (km.supportClipboardEvent && !kity.Browser.gecko) {
+                var Copy = function(e) {
+                    this.fire("beforeCopy", e);
+                };
+                var Cut = function(e) {
+                    this.fire("beforeCut", e);
+                };
+                var Paste = function(e) {
+                    this.fire("beforePaste", e);
+                };
+                return {
+                    commands: {
+                        copy: CopyCommand,
+                        cut: CutCommand,
+                        paste: PasteCommand
+                    },
+                    clipBoardEvents: {
+                        copy: Copy.bind(km),
+                        cut: Cut.bind(km),
+                        paste: Paste.bind(km)
+                    },
+                    sendToClipboard: sendToClipboard
+                };
+            } else {
+                return {
+                    commands: {
+                        copy: CopyCommand,
+                        cut: CutCommand,
+                        paste: PasteCommand
+                    },
+                    commandShortcutKeys: {
+                        copy: "normal::ctrl+c|",
+                        cut: "normal::ctrl+x",
+                        paste: "normal::ctrl+v"
+                    },
+                    sendToClipboard: sendToClipboard
+                };
+            }
+        });
+    }
+};
+
+//src/module/dragtree.js
+_p[45] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        // 矩形的变形动画定义
+        var MoveToParentCommand = kity.createClass("MoveToParentCommand", {
+            base: Command,
+            execute: function(minder, nodes, parent) {
+                var node;
+                for (var i = 0; i < nodes.length; i++) {
+                    node = nodes[i];
+                    if (node.parent) {
+                        node.parent.removeChild(node);
+                        parent.appendChild(node);
+                        node.render();
+                    }
+                }
+                parent.expand();
+                minder.select(nodes, true);
+            }
+        });
+        var DropHinter = kity.createClass("DropHinter", {
+            base: kity.Group,
+            constructor: function() {
+                this.callBase();
+                this.rect = new kity.Rect();
+                this.addShape(this.rect);
+            },
+            render: function(target) {
+                this.setVisible(!!target);
+                if (target) {
+                    this.rect.setBox(target.getLayoutBox()).setRadius(target.getStyle("radius") || 0).stroke(target.getStyle("drop-hint-color") || "yellow", target.getStyle("drop-hint-width") || 2);
+                    this.bringTop();
+                }
+            }
+        });
+        var OrderHinter = kity.createClass("OrderHinter", {
+            base: kity.Group,
+            constructor: function() {
+                this.callBase();
+                this.area = new kity.Rect();
+                this.path = new kity.Path();
+                this.addShapes([ this.area, this.path ]);
+            },
+            render: function(hint) {
+                this.setVisible(!!hint);
+                if (hint) {
+                    this.area.setBox(hint.area);
+                    this.area.fill(hint.node.getStyle("order-hint-area-color") || "rgba(0, 255, 0, .5)");
+                    this.path.setPathData(hint.path);
+                    this.path.stroke(hint.node.getStyle("order-hint-path-color") || "#0f0", hint.node.getStyle("order-hint-path-width") || 1);
+                }
+            }
+        });
+        // 对拖动对象的一个替代盒子,控制整个拖放的逻辑,包括:
+        //    1. 从节点列表计算出拖动部分
+        //    2. 计算可以 drop 的节点,产生 drop 交互提示
+        var TreeDragger = kity.createClass("TreeDragger", {
+            constructor: function(minder) {
+                this._minder = minder;
+                this._dropHinter = new DropHinter();
+                this._orderHinter = new OrderHinter();
+                minder.getRenderContainer().addShapes([ this._dropHinter, this._orderHinter ]);
+            },
+            dragStart: function(position) {
+                // 只记录开始位置,不马上开启拖放模式
+                // 这个位置同时是拖放范围收缩时的焦点位置(中心)
+                this._startPosition = position;
+            },
+            dragMove: function(position) {
+                // 启动拖放模式需要最小的移动距离
+                var DRAG_MOVE_THRESHOLD = 10;
+                if (!this._startPosition) return;
+                var movement = kity.Vector.fromPoints(this._dragPosition || this._startPosition, position);
+                var minder = this._minder;
+                this._dragPosition = position;
+                if (!this._dragMode) {
+                    // 判断拖放模式是否该启动
+                    if (kity.Vector.fromPoints(this._dragPosition, this._startPosition).length() < DRAG_MOVE_THRESHOLD) {
+                        return;
+                    }
+                    if (!this._enterDragMode()) {
+                        return;
+                    }
+                }
+                for (var i = 0; i < this._dragSources.length; i++) {
+                    this._dragSources[i].setLayoutOffset(this._dragSources[i].getLayoutOffset().offset(movement));
+                    minder.applyLayoutResult(this._dragSources[i]);
+                }
+                if (!this._dropTest()) {
+                    this._orderTest();
+                } else {
+                    this._renderOrderHint(this._orderSucceedHint = null);
+                }
+            },
+            dragEnd: function() {
+                this._startPosition = null;
+                this._dragPosition = null;
+                if (!this._dragMode) {
+                    return;
+                }
+                this._fadeDragSources(1);
+                if (this._dropSucceedTarget) {
+                    this._dragSources.forEach(function(source) {
+                        source.setLayoutOffset(null);
+                    });
+                    this._minder.layout(-1);
+                    this._minder.execCommand("movetoparent", this._dragSources, this._dropSucceedTarget);
+                } else if (this._orderSucceedHint) {
+                    var hint = this._orderSucceedHint;
+                    var index = hint.node.getIndex();
+                    var sourceIndexes = this._dragSources.map(function(source) {
+                        // 顺便干掉布局偏移
+                        source.setLayoutOffset(null);
+                        return source.getIndex();
+                    });
+                    var maxIndex = Math.max.apply(Math, sourceIndexes);
+                    var minIndex = Math.min.apply(Math, sourceIndexes);
+                    if (index < minIndex && hint.type == "down") index++;
+                    if (index > maxIndex && hint.type == "up") index--;
+                    hint.node.setLayoutOffset(null);
+                    this._minder.execCommand("arrange", index);
+                    this._renderOrderHint(null);
+                } else {
+                    this._minder.fire("savescene");
+                }
+                this._minder.layout(300);
+                this._leaveDragMode();
+                this._minder.fire("contentchange");
+            },
+            // 进入拖放模式:
+            //    1. 计算拖放源和允许的拖放目标
+            //    2. 标记已启动
+            _enterDragMode: function() {
+                this._calcDragSources();
+                if (!this._dragSources.length) {
+                    this._startPosition = null;
+                    return false;
+                }
+                this._fadeDragSources(.5);
+                this._calcDropTargets();
+                this._calcOrderHints();
+                this._dragMode = true;
+                this._minder.setStatus("dragtree");
+                return true;
+            },
+            // 从选中的节点计算拖放源
+            //    并不是所有选中的节点都作为拖放源,如果选中节点中存在 A 和 B,
+            //    并且 A 是 B 的祖先,则 B 不作为拖放源
+            //
+            //    计算过程:
+            //       1. 将节点按照树高排序,排序后只可能是前面节点是后面节点的祖先
+            //       2. 从后往前枚举排序的结果,如果发现枚举目标之前存在其祖先,
+            //          则排除枚举目标作为拖放源,否则加入拖放源
+            _calcDragSources: function() {
+                this._dragSources = this._minder.getSelectedAncestors();
+            },
+            _fadeDragSources: function(opacity) {
+                var minder = this._minder;
+                this._dragSources.forEach(function(source) {
+                    source.getRenderContainer().setOpacity(opacity, 200);
+                    source.traverse(function(node) {
+                        if (opacity < 1) {
+                            minder.detachNode(node);
+                        } else {
+                            minder.attachNode(node);
+                        }
+                    }, true);
+                });
+            },
+            // 计算拖放目标可以释放的节点列表(释放意味着成为其子树),存在这条限制规则:
+            //    - 不能拖放到拖放目标的子树上(允许拖放到自身,因为多选的情况下可以把其它节点加入)
+            //
+            //    1. 加入当前节点(初始为根节点)到允许列表
+            //    2. 对于当前节点的每一个子节点:
+            //       (1) 如果是拖放目标的其中一个节点,忽略(整棵子树被剪枝)
+            //       (2) 如果不是拖放目标之一,以当前子节点为当前节点,回到 1 计算
+            //    3. 返回允许列表
+            //
+            _calcDropTargets: function() {
+                function findAvailableParents(nodes, root) {
+                    var availables = [], i;
+                    availables.push(root);
+                    root.getChildren().forEach(function(test) {
+                        for (i = 0; i < nodes.length; i++) {
+                            if (nodes[i] == test) return;
+                        }
+                        availables = availables.concat(findAvailableParents(nodes, test));
+                    });
+                    return availables;
+                }
+                this._dropTargets = findAvailableParents(this._dragSources, this._minder.getRoot());
+                this._dropTargetBoxes = this._dropTargets.map(function(source) {
+                    return source.getLayoutBox();
+                });
+            },
+            _calcOrderHints: function() {
+                var sources = this._dragSources;
+                var ancestor = MinderNode.getCommonAncestor(sources);
+                // 只有一个元素选中,公共祖先是其父
+                if (ancestor == sources[0]) ancestor = sources[0].parent;
+                if (sources.length === 0 || ancestor != sources[0].parent) {
+                    this._orderHints = [];
+                    return;
+                }
+                var siblings = ancestor.children;
+                this._orderHints = siblings.reduce(function(hint, sibling) {
+                    if (sources.indexOf(sibling) == -1) {
+                        hint = hint.concat(sibling.getOrderHint());
+                    }
+                    return hint;
+                }, []);
+            },
+            _leaveDragMode: function() {
+                this._dragMode = false;
+                this._dropSucceedTarget = null;
+                this._orderSucceedHint = null;
+                this._renderDropHint(null);
+                this._renderOrderHint(null);
+                this._minder.rollbackStatus();
+            },
+            _drawForDragMode: function() {
+                this._text.setContent(this._dragSources.length + " items");
+                this._text.setPosition(this._startPosition.x, this._startPosition.y + 5);
+                this._minder.getRenderContainer().addShape(this);
+            },
+            /**
+         * 通过 judge 函数判断 targetBox 和 sourceBox 的位置交叉关系
+         * @param targets -- 目标节点
+         * @param targetBoxMapper -- 目标节点与对应 Box 的映射关系
+         * @param judge -- 判断函数
+         * @returns {*}
+         * @private
+         */
+            _boxTest: function(targets, targetBoxMapper, judge) {
+                var sourceBoxes = this._dragSources.map(function(source) {
+                    return source.getLayoutBox();
+                });
+                var i, j, target, sourceBox, targetBox;
+                judge = judge || function(intersectBox, sourceBox, targetBox) {
+                    return intersectBox && !intersectBox.isEmpty();
+                };
+                for (i = 0; i < targets.length; i++) {
+                    target = targets[i];
+                    targetBox = targetBoxMapper.call(this, target, i);
+                    for (j = 0; j < sourceBoxes.length; j++) {
+                        sourceBox = sourceBoxes[j];
+                        var intersectBox = sourceBox.intersect(targetBox);
+                        if (judge(intersectBox, sourceBox, targetBox)) {
+                            return target;
+                        }
+                    }
+                }
+                return null;
+            },
+            _dropTest: function() {
+                this._dropSucceedTarget = this._boxTest(this._dropTargets, function(target, i) {
+                    return this._dropTargetBoxes[i];
+                }, function(intersectBox, sourceBox, targetBox) {
+                    function area(box) {
+                        return box.width * box.height;
+                    }
+                    if (!intersectBox) return false;
+                    /*
+                * Added by zhangbobell, 2015.9.8
+                *
+                * 增加了下面一行判断,修复了循环比较中 targetBox 为折叠节点时,intersetBox 面积为 0,
+                * 而 targetBox 的 width 和 height 均为 0
+                * 此时造成了满足以下的第二个条件而返回 true
+                * */
+                    if (!area(intersectBox)) return false;
+                    // 面积判断,交叉面积大于其中的一半
+                    if (area(intersectBox) > .5 * Math.min(area(sourceBox), area(targetBox))) return true;
+                    // 有一个边完全重合的情况,也认为两个是交叉的
+                    if (intersectBox.width + 1 >= Math.min(sourceBox.width, targetBox.width)) return true;
+                    if (intersectBox.height + 1 >= Math.min(sourceBox.height, targetBox.height)) return true;
+                    return false;
+                });
+                this._renderDropHint(this._dropSucceedTarget);
+                return !!this._dropSucceedTarget;
+            },
+            _orderTest: function() {
+                this._orderSucceedHint = this._boxTest(this._orderHints, function(hint) {
+                    return hint.area;
+                });
+                this._renderOrderHint(this._orderSucceedHint);
+                return !!this._orderSucceedHint;
+            },
+            _renderDropHint: function(target) {
+                this._dropHinter.render(target);
+            },
+            _renderOrderHint: function(hint) {
+                this._orderHinter.render(hint);
+            },
+            preventDragMove: function() {
+                this._startPosition = null;
+            }
+        });
+        Module.register("DragTree", function() {
+            var dragger;
+            return {
+                init: function() {
+                    dragger = new TreeDragger(this);
+                    window.addEventListener("mouseup", function() {
+                        dragger.dragEnd();
+                    });
+                },
+                events: {
+                    "normal.mousedown inputready.mousedown": function(e) {
+                        // 单选中根节点也不触发拖拽
+                        if (e.originEvent.button) return;
+                        if (e.getTargetNode() && e.getTargetNode() != this.getRoot()) {
+                            dragger.dragStart(e.getPosition());
+                        }
+                    },
+                    "normal.mousemove dragtree.mousemove": function(e) {
+                        dragger.dragMove(e.getPosition());
+                    },
+                    "normal.mouseup dragtree.beforemouseup": function(e) {
+                        dragger.dragEnd();
+                        //e.stopPropagation();
+                        e.preventDefault();
+                    },
+                    statuschange: function(e) {
+                        if (e.lastStatus == "textedit" && e.currentStatus == "normal") {
+                            dragger.preventDragMove();
+                        }
+                    }
+                },
+                commands: {
+                    movetoparent: MoveToParentCommand
+                }
+            };
+        });
+    }
+};
+
+//src/module/expand.js
+_p[46] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var keymap = _p.r(15);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("Expand", function() {
+            var minder = this;
+            var EXPAND_STATE_DATA = "expandState", STATE_EXPAND = "expand", STATE_COLLAPSE = "collapse";
+            // 将展开的操作和状态读取接口拓展到 MinderNode 上
+            kity.extendClass(MinderNode, {
+                /**
+             * 展开节点
+             * @param  {Policy} policy 展开的策略,默认为 KEEP_STATE
+             */
+                expand: function() {
+                    this.setData(EXPAND_STATE_DATA, STATE_EXPAND);
+                    return this;
+                },
+                /**
+             * 收起节点
+             */
+                collapse: function() {
+                    this.setData(EXPAND_STATE_DATA, STATE_COLLAPSE);
+                    return this;
+                },
+                /**
+             * 判断节点当前的状态是否为展开
+             */
+                isExpanded: function() {
+                    var expanded = this.getData(EXPAND_STATE_DATA) !== STATE_COLLAPSE;
+                    return expanded && (this.isRoot() || this.parent.isExpanded());
+                },
+                /**
+             * 判断节点当前的状态是否为收起
+             */
+                isCollapsed: function() {
+                    return !this.isExpanded();
+                }
+            });
+            /**
+         * @command Expand
+         * @description 展开当前选中的节点,保证其可见
+         * @param {bool} justParents 是否只展开到父亲
+         *     * `false` - (默认)保证选中的节点以及其子树可见
+         *     * `true` - 只保证选中的节点可见,不展开其子树
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+            var ExpandCommand = kity.createClass("ExpandCommand", {
+                base: Command,
+                execute: function(km, justParents) {
+                    var node = km.getSelectedNode();
+                    if (!node) return;
+                    if (justParents) {
+                        node = node.parent;
+                    }
+                    while (node.parent) {
+                        node.expand();
+                        node = node.parent;
+                    }
+                    node.renderTree();
+                    km.layout(100);
+                },
+                queryState: function(km) {
+                    var node = km.getSelectedNode();
+                    return node && !node.isRoot() && !node.isExpanded() ? 0 : -1;
+                }
+            });
+            /**
+         * @command ExpandToLevel
+         * @description 展开脑图到指定的层级
+         * @param {number} level 指定展开到的层级,最少值为 1。
+         * @state
+         *   0: 一直可用
+         */
+            var ExpandToLevelCommand = kity.createClass("ExpandToLevelCommand", {
+                base: Command,
+                execute: function(km, level) {
+                    km.getRoot().traverse(function(node) {
+                        if (node.getLevel() < level) node.expand();
+                        if (node.getLevel() == level && !node.isLeaf()) node.collapse();
+                    });
+                    km.refresh(100);
+                },
+                enableReadOnly: true
+            });
+            /**
+         * @command Collapse
+         * @description 收起当前节点的子树
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+            var CollapseCommand = kity.createClass("CollapseCommand", {
+                base: Command,
+                execute: function(km) {
+                    var node = km.getSelectedNode();
+                    if (!node) return;
+                    node.collapse();
+                    node.renderTree();
+                    km.layout();
+                },
+                queryState: function(km) {
+                    var node = km.getSelectedNode();
+                    return node && !node.isRoot() && node.isExpanded() ? 0 : -1;
+                }
+            });
+            var Expander = kity.createClass("Expander", {
+                base: kity.Group,
+                constructor: function(node) {
+                    this.callBase();
+                    this.radius = 6;
+                    this.outline = new kity.Circle(this.radius).stroke("gray").fill("white");
+                    this.sign = new kity.Path().stroke("gray");
+                    this.addShapes([ this.outline, this.sign ]);
+                    this.initEvent(node);
+                    this.setId(utils.uuid("node_expander"));
+                    this.setStyle("cursor", "pointer");
+                },
+                initEvent: function(node) {
+                    this.on("mousedown", function(e) {
+                        minder.select([ node ], true);
+                        if (node.isExpanded()) {
+                            node.collapse();
+                        } else {
+                            node.expand();
+                        }
+                        node.renderTree().getMinder().layout(100);
+                        node.getMinder().fire("contentchange");
+                        e.stopPropagation();
+                        e.preventDefault();
+                    });
+                    this.on("dblclick click mouseup", function(e) {
+                        e.stopPropagation();
+                        e.preventDefault();
+                    });
+                },
+                setState: function(state) {
+                    if (state == "hide") {
+                        this.setVisible(false);
+                        return;
+                    }
+                    this.setVisible(true);
+                    var pathData = [ "M", 1.5 - this.radius, 0, "L", this.radius - 1.5, 0 ];
+                    if (state == STATE_COLLAPSE) {
+                        pathData.push([ "M", 0, 1.5 - this.radius, "L", 0, this.radius - 1.5 ]);
+                    }
+                    this.sign.setPathData(pathData);
+                }
+            });
+            var ExpanderRenderer = kity.createClass("ExpanderRenderer", {
+                base: Renderer,
+                create: function(node) {
+                    if (node.isRoot()) return;
+                    this.expander = new Expander(node);
+                    node.getRenderContainer().prependShape(this.expander);
+                    node.expanderRenderer = this;
+                    this.node = node;
+                    return this.expander;
+                },
+                shouldRender: function(node) {
+                    return !node.isRoot();
+                },
+                update: function(expander, node, box) {
+                    if (!node.parent) return;
+                    var visible = node.parent.isExpanded();
+                    expander.setState(visible && node.children.length ? node.getData(EXPAND_STATE_DATA) : "hide");
+                    var vector = node.getLayoutVectorIn().normalize(expander.radius + node.getStyle("stroke-width"));
+                    var position = node.getVertexIn().offset(vector.reverse());
+                    this.expander.setTranslate(position);
+                }
+            });
+            return {
+                commands: {
+                    expand: ExpandCommand,
+                    expandtolevel: ExpandToLevelCommand,
+                    collapse: CollapseCommand
+                },
+                events: {
+                    layoutapply: function(e) {
+                        var r = e.node.getRenderer("ExpanderRenderer");
+                        if (r.getRenderShape()) {
+                            r.update(r.getRenderShape(), e.node);
+                        }
+                    },
+                    beforerender: function(e) {
+                        var node = e.node;
+                        var visible = !node.parent || node.parent.isExpanded();
+                        var minder = this;
+                        node.getRenderContainer().setVisible(visible);
+                        if (!visible) e.stopPropagation();
+                    },
+                    "normal.keydown": function(e) {
+                        if (this.getStatus() == "textedit") return;
+                        if (e.originEvent.keyCode == keymap["/"]) {
+                            var node = this.getSelectedNode();
+                            if (!node || node == this.getRoot()) return;
+                            var expanded = node.isExpanded();
+                            this.getSelectedNodes().forEach(function(node) {
+                                if (expanded) node.collapse(); else node.expand();
+                                node.renderTree();
+                            });
+                            this.layout(100);
+                            this.fire("contentchange");
+                            e.preventDefault();
+                            e.stopPropagationImmediately();
+                        }
+                        if (e.isShortcutKey("Alt+`")) {
+                            this.execCommand("expandtolevel", 9999);
+                        }
+                        for (var i = 1; i < 6; i++) {
+                            if (e.isShortcutKey("Alt+" + i)) {
+                                this.execCommand("expandtolevel", i);
+                            }
+                        }
+                    }
+                },
+                renderers: {
+                    outside: ExpanderRenderer
+                },
+                contextmenu: [ {
+                    command: "expandtoleaf",
+                    query: function() {
+                        return !minder.getSelectedNode();
+                    },
+                    fn: function(minder) {
+                        minder.execCommand("expandtolevel", 9999);
+                    }
+                }, {
+                    command: "expandtolevel1",
+                    query: function() {
+                        return !minder.getSelectedNode();
+                    },
+                    fn: function(minder) {
+                        minder.execCommand("expandtolevel", 1);
+                    }
+                }, {
+                    command: "expandtolevel2",
+                    query: function() {
+                        return !minder.getSelectedNode();
+                    },
+                    fn: function(minder) {
+                        minder.execCommand("expandtolevel", 2);
+                    }
+                }, {
+                    command: "expandtolevel3",
+                    query: function() {
+                        return !minder.getSelectedNode();
+                    },
+                    fn: function(minder) {
+                        minder.execCommand("expandtolevel", 3);
+                    }
+                }, {
+                    divider: true
+                } ]
+            };
+        });
+    }
+};
+
+//src/module/font.js
+_p[47] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var TextRenderer = _p.r(61);
+        function getNodeDataOrStyle(node, name) {
+            return node.getData(name) || node.getStyle(name);
+        }
+        TextRenderer.registerStyleHook(function(node, textGroup) {
+            var dataColor = node.getData("color");
+            var selectedColor = node.getStyle("selected-color");
+            var styleColor = node.getStyle("color");
+            var foreColor = dataColor || (node.isSelected() && selectedColor ? selectedColor : styleColor);
+            var fontFamily = getNodeDataOrStyle(node, "font-family");
+            var fontSize = getNodeDataOrStyle(node, "font-size");
+            textGroup.fill(foreColor);
+            textGroup.eachItem(function(index, item) {
+                item.setFont({
+                    family: fontFamily,
+                    size: fontSize
+                });
+            });
+        });
+        Module.register("fontmodule", {
+            commands: {
+                /**
+             * @command ForeColor
+             * @description 设置选中节点的字体颜色
+             * @param {string} color 表示颜色的字符串
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 如果只有一个节点选中,返回已选中节点的字体颜色;否则返回 'mixed'。
+             */
+                forecolor: kity.createClass("fontcolorCommand", {
+                    base: Command,
+                    execute: function(km, color) {
+                        var nodes = km.getSelectedNodes();
+                        nodes.forEach(function(n) {
+                            n.setData("color", color);
+                            n.render();
+                        });
+                    },
+                    queryState: function(km) {
+                        return km.getSelectedNodes().length === 0 ? -1 : 0;
+                    },
+                    queryValue: function(km) {
+                        if (km.getSelectedNodes().length == 1) {
+                            return km.getSelectedNodes()[0].getData("color");
+                        }
+                        return "mixed";
+                    }
+                }),
+                /**
+             * @command Background
+             * @description 设置选中节点的背景颜色
+             * @param {string} color 表示颜色的字符串
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 如果只有一个节点选中,返回已选中节点的背景颜色;否则返回 'mixed'。
+             */
+                background: kity.createClass("backgroudCommand", {
+                    base: Command,
+                    execute: function(km, color) {
+                        var nodes = km.getSelectedNodes();
+                        nodes.forEach(function(n) {
+                            n.setData("background", color);
+                            n.render();
+                        });
+                    },
+                    queryState: function(km) {
+                        return km.getSelectedNodes().length === 0 ? -1 : 0;
+                    },
+                    queryValue: function(km) {
+                        if (km.getSelectedNodes().length == 1) {
+                            return km.getSelectedNodes()[0].getData("background");
+                        }
+                        return "mixed";
+                    }
+                }),
+                /**
+             * @command FontFamily
+             * @description 设置选中节点的字体
+             * @param {string} family 表示字体的字符串
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 返回首个选中节点的字体
+             */
+                fontfamily: kity.createClass("fontfamilyCommand", {
+                    base: Command,
+                    execute: function(km, family) {
+                        var nodes = km.getSelectedNodes();
+                        nodes.forEach(function(n) {
+                            n.setData("font-family", family);
+                            n.render();
+                            km.layout();
+                        });
+                    },
+                    queryState: function(km) {
+                        return km.getSelectedNodes().length === 0 ? -1 : 0;
+                    },
+                    queryValue: function(km) {
+                        var node = km.getSelectedNode();
+                        if (node) return node.getData("font-family");
+                        return null;
+                    }
+                }),
+                /**
+             * @command FontSize
+             * @description 设置选中节点的字体大小
+             * @param {number} size 字体大小(px)
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 返回首个选中节点的字体大小
+             */
+                fontsize: kity.createClass("fontsizeCommand", {
+                    base: Command,
+                    execute: function(km, size) {
+                        var nodes = km.getSelectedNodes();
+                        nodes.forEach(function(n) {
+                            n.setData("font-size", size);
+                            n.render();
+                            km.layout(300);
+                        });
+                    },
+                    queryState: function(km) {
+                        return km.getSelectedNodes().length === 0 ? -1 : 0;
+                    },
+                    queryValue: function(km) {
+                        var node = km.getSelectedNode();
+                        if (node) return node.getData("font-size");
+                        return null;
+                    }
+                })
+            }
+        });
+    }
+};
+
+//src/module/hyperlink.js
+_p[48] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        // jscs:disable maximumLineLength
+        var linkShapePath = "M16.614,10.224h-1.278c-1.668,0-3.07-1.07-3.599-2.556h4.877c0.707,0,1.278-0.571,1.278-1.278V3.834 c0-0.707-0.571-1.278-1.278-1.278h-4.877C12.266,1.071,13.668,0,15.336,0h1.278c2.116,0,3.834,1.716,3.834,3.834V6.39 C20.448,8.508,18.73,10.224,16.614,10.224z M5.112,5.112c0-0.707,0.573-1.278,1.278-1.278h7.668c0.707,0,1.278,0.571,1.278,1.278 S14.765,6.39,14.058,6.39H6.39C5.685,6.39,5.112,5.819,5.112,5.112z M2.556,3.834V6.39c0,0.707,0.573,1.278,1.278,1.278h4.877 c-0.528,1.486-1.932,2.556-3.599,2.556H3.834C1.716,10.224,0,8.508,0,6.39V3.834C0,1.716,1.716,0,3.834,0h1.278 c1.667,0,3.071,1.071,3.599,2.556H3.834C3.129,2.556,2.556,3.127,2.556,3.834z";
+        Module.register("hyperlink", {
+            commands: {
+                /**
+             * @command HyperLink
+             * @description 为选中的节点添加超链接
+             * @param {string} url 超链接的 URL,设置为 null 移除
+             * @param {string} title 超链接的说明
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 返回首个选中节点的超链接信息,JSON 对象: `{url: url, title: title}`
+             */
+                hyperlink: kity.createClass("hyperlink", {
+                    base: Command,
+                    execute: function(km, url, title) {
+                        var nodes = km.getSelectedNodes();
+                        nodes.forEach(function(n) {
+                            n.setData("hyperlink", url);
+                            n.setData("hyperlinkTitle", url && title);
+                            n.render();
+                        });
+                        km.layout();
+                    },
+                    queryState: function(km) {
+                        var nodes = km.getSelectedNodes(), result = 0;
+                        if (nodes.length === 0) {
+                            return -1;
+                        }
+                        nodes.forEach(function(n) {
+                            if (n && n.getData("hyperlink")) {
+                                result = 0;
+                                return false;
+                            }
+                        });
+                        return result;
+                    },
+                    queryValue: function(km) {
+                        var node = km.getSelectedNode();
+                        return {
+                            url: node.getData("hyperlink"),
+                            title: node.getData("hyperlinkTitle")
+                        };
+                    }
+                })
+            },
+            renderers: {
+                right: kity.createClass("hyperlinkrender", {
+                    base: Renderer,
+                    create: function() {
+                        var link = new kity.HyperLink();
+                        var linkshape = new kity.Path();
+                        var outline = new kity.Rect(24, 22, -2, -6, 4).fill("rgba(255, 255, 255, 0)");
+                        linkshape.setPathData(linkShapePath).fill("#666");
+                        link.addShape(outline);
+                        link.addShape(linkshape);
+                        link.setTarget("_blank");
+                        link.setStyle("cursor", "pointer");
+                        link.on("mouseover", function() {
+                            outline.fill("rgba(255, 255, 200, .8)");
+                        }).on("mouseout", function() {
+                            outline.fill("rgba(255, 255, 255, 0)");
+                        });
+                        return link;
+                    },
+                    shouldRender: function(node) {
+                        return node.getData("hyperlink");
+                    },
+                    update: function(link, node, box) {
+                        var href = node.getData("hyperlink");
+                        link.setHref("#");
+                        var allowed = [ "^http:", "^https:", "^ftp:", "^mailto:" ];
+                        for (var i = 0; i < allowed.length; i++) {
+                            var regex = new RegExp(allowed[i]);
+                            if (regex.test(href)) {
+                                link.setHref(href);
+                                break;
+                            }
+                        }
+                        var title = node.getData("hyperlinkTitle");
+                        if (title) {
+                            title = [ title, "(", href, ")" ].join("");
+                        } else {
+                            title = href;
+                        }
+                        link.node.setAttributeNS("http://www.w3.org/1999/xlink", "title", title);
+                        var spaceRight = node.getStyle("space-right");
+                        link.setTranslate(box.right + spaceRight + 2, -5);
+                        return new kity.Box({
+                            x: box.right + spaceRight,
+                            y: -11,
+                            width: 24,
+                            height: 22
+                        });
+                    }
+                })
+            }
+        });
+    }
+};
+
+//src/module/image-viewer.js
+_p[49] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var keymap = _p.r(15);
+        var Module = _p.r(20);
+        var Command = _p.r(9);
+        Module.register("ImageViewer", function() {
+            function createEl(name, classNames, children) {
+                var el = document.createElement(name);
+                addClass(el, classNames);
+                children && children.length && children.forEach(function(child) {
+                    el.appendChild(child);
+                });
+                return el;
+            }
+            function on(el, event, handler) {
+                el.addEventListener(event, handler);
+            }
+            function addClass(el, classNames) {
+                classNames && classNames.split(" ").forEach(function(className) {
+                    el.classList.add(className);
+                });
+            }
+            function removeClass(el, classNames) {
+                classNames && classNames.split(" ").forEach(function(className) {
+                    el.classList.remove(className);
+                });
+            }
+            var ImageViewer = kity.createClass("ImageViewer", {
+                constructor: function() {
+                    var btnClose = createEl("button", "km-image-viewer-btn km-image-viewer-close");
+                    var btnSource = createEl("button", "km-image-viewer-btn km-image-viewer-source");
+                    var image = this.image = createEl("img");
+                    var toolbar = this.toolbar = createEl("div", "km-image-viewer-toolbar", [ btnSource, btnClose ]);
+                    var container = createEl("div", "km-image-viewer-container", [ image ]);
+                    var viewer = this.viewer = createEl("div", "km-image-viewer", [ toolbar, container ]);
+                    this.hotkeyHandler = this.hotkeyHandler.bind(this);
+                    on(btnClose, "click", this.close.bind(this));
+                    on(btnSource, "click", this.viewSource.bind(this));
+                    on(image, "click", this.zoomImage.bind(this));
+                    on(viewer, "contextmenu", this.toggleToolbar.bind(this));
+                    on(document, "keydown", this.hotkeyHandler);
+                },
+                dispose: function() {
+                    this.close();
+                    document.removeEventListener("remove", this.hotkeyHandler);
+                },
+                hotkeyHandler: function(e) {
+                    if (!this.actived) {
+                        return;
+                    }
+                    if (e.keyCode === keymap["esc"]) {
+                        this.close();
+                    }
+                },
+                toggleToolbar: function(e) {
+                    e && e.preventDefault();
+                    this.toolbar.classList.toggle("hidden");
+                },
+                zoomImage: function(restore) {
+                    var image = this.image;
+                    if (typeof restore === "boolean") {
+                        restore && addClass(image, "limited");
+                    } else {
+                        image.classList.toggle("limited");
+                    }
+                },
+                viewSource: function(src) {
+                    window.open(this.image.src);
+                },
+                open: function(src) {
+                    var input = document.querySelector("input");
+                    if (input) {
+                        input.focus();
+                        input.blur();
+                    }
+                    this.image.src = src;
+                    this.zoomImage(true);
+                    document.body.appendChild(this.viewer);
+                    this.actived = true;
+                },
+                close: function() {
+                    this.image.src = "";
+                    document.body.removeChild(this.viewer);
+                    this.actived = false;
+                }
+            });
+            return {
+                init: function() {
+                    this.viewer = new ImageViewer();
+                },
+                events: {
+                    "normal.dblclick": function(e) {
+                        var shape = e.kityEvent.targetShape;
+                        if (shape.__KityClassName === "Image" && shape.url) {
+                            this.viewer.open(shape.url);
+                        }
+                    }
+                }
+            };
+        });
+    }
+};
+
+//src/module/image.js
+_p[50] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("image", function() {
+            function loadImageSize(url, callback) {
+                var img = document.createElement("img");
+                img.onload = function() {
+                    callback(img.width, img.height);
+                };
+                img.onerror = function() {
+                    callback(null);
+                };
+                img.src = url;
+            }
+            function fitImageSize(width, height, maxWidth, maxHeight) {
+                var ratio = width / height, fitRatio = maxWidth / maxHeight;
+                // 宽高比大于最大尺寸的宽高比,以宽度为标准适应
+                if (width > maxWidth && ratio > fitRatio) {
+                    width = maxWidth;
+                    height = width / ratio;
+                } else if (height > maxHeight) {
+                    height = maxHeight;
+                    width = height * ratio;
+                }
+                return {
+                    width: width | 0,
+                    height: height | 0
+                };
+            }
+            /**
+         * @command Image
+         * @description 为选中的节点添加图片
+         * @param {string} url 图片的 URL,设置为 null 移除
+         * @param {string} title 图片的说明
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         * @return 返回首个选中节点的图片信息,JSON 对象: `{url: url, title: title}`
+         */
+            var ImageCommand = kity.createClass("ImageCommand", {
+                base: Command,
+                execute: function(km, url, title) {
+                    var nodes = km.getSelectedNodes();
+                    loadImageSize(url, function(width, height) {
+                        nodes.forEach(function(n) {
+                            var size = fitImageSize(width, height, km.getOption("maxImageWidth"), km.getOption("maxImageHeight"));
+                            n.setData("image", url);
+                            n.setData("imageTitle", url && title);
+                            n.setData("imageSize", url && size);
+                            n.render();
+                        });
+                        km.fire("saveScene");
+                        km.layout(300);
+                    });
+                },
+                queryState: function(km) {
+                    var nodes = km.getSelectedNodes(), result = 0;
+                    if (nodes.length === 0) {
+                        return -1;
+                    }
+                    nodes.forEach(function(n) {
+                        if (n && n.getData("image")) {
+                            result = 0;
+                            return false;
+                        }
+                    });
+                    return result;
+                },
+                queryValue: function(km) {
+                    var node = km.getSelectedNode();
+                    return {
+                        url: node.getData("image"),
+                        title: node.getData("imageTitle")
+                    };
+                }
+            });
+            var ImageRenderer = kity.createClass("ImageRenderer", {
+                base: Renderer,
+                create: function(node) {
+                    return new kity.Image(node.getData("image"));
+                },
+                shouldRender: function(node) {
+                    return node.getData("image");
+                },
+                update: function(image, node, box) {
+                    var url = node.getData("image");
+                    var title = node.getData("imageTitle");
+                    var size = node.getData("imageSize");
+                    var spaceTop = node.getStyle("space-top");
+                    if (!size) return;
+                    if (title) {
+                        image.node.setAttributeNS("http://www.w3.org/1999/xlink", "title", title);
+                    }
+                    var x = box.cx - size.width / 2;
+                    var y = box.y - size.height - spaceTop;
+                    image.setUrl(url).setX(x | 0).setY(y | 0).setWidth(size.width | 0).setHeight(size.height | 0);
+                    return new kity.Box(x | 0, y | 0, size.width | 0, size.height | 0);
+                }
+            });
+            return {
+                defaultOptions: {
+                    maxImageWidth: 200,
+                    maxImageHeight: 200
+                },
+                commands: {
+                    image: ImageCommand
+                },
+                renderers: {
+                    top: ImageRenderer
+                }
+            };
+        });
+    }
+};
+
+//src/module/keynav.js
+_p[51] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var keymap = _p.r(15);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("KeyboardModule", function() {
+            var min = Math.min, max = Math.max, abs = Math.abs, sqrt = Math.sqrt, exp = Math.exp;
+            function buildPositionNetwork(root) {
+                var pointIndexes = [], p;
+                root.traverse(function(node) {
+                    p = node.getLayoutBox();
+                    // bugfix: 不应导航到收起的节点(判断其尺寸是否存在)
+                    if (p.width && p.height) {
+                        pointIndexes.push({
+                            left: p.x,
+                            top: p.y,
+                            right: p.x + p.width,
+                            bottom: p.y + p.height,
+                            width: p.width,
+                            height: p.height,
+                            node: node
+                        });
+                    }
+                });
+                for (var i = 0; i < pointIndexes.length; i++) {
+                    findClosestPointsFor(pointIndexes, i);
+                }
+            }
+            // 这是金泉的点子,赞!
+            // 求两个不相交矩形的最近距离
+            function getCoefedDistance(box1, box2) {
+                var xMin, xMax, yMin, yMax, xDist, yDist, dist, cx, cy;
+                xMin = min(box1.left, box2.left);
+                xMax = max(box1.right, box2.right);
+                yMin = min(box1.top, box2.top);
+                yMax = max(box1.bottom, box2.bottom);
+                xDist = xMax - xMin - box1.width - box2.width;
+                yDist = yMax - yMin - box1.height - box2.height;
+                if (xDist < 0) dist = yDist; else if (yDist < 0) dist = xDist; else dist = sqrt(xDist * xDist + yDist * yDist);
+                var node1 = box1.node;
+                var node2 = box2.node;
+                // sibling
+                if (node1.parent == node2.parent) {
+                    dist /= 10;
+                }
+                // parent
+                if (node2.parent == node1) {
+                    dist /= 5;
+                }
+                return dist;
+            }
+            function findClosestPointsFor(pointIndexes, iFind) {
+                var find = pointIndexes[iFind];
+                var most = {}, quad;
+                var current, dist;
+                for (var i = 0; i < pointIndexes.length; i++) {
+                    if (i == iFind) continue;
+                    current = pointIndexes[i];
+                    dist = getCoefedDistance(current, find);
+                    // left check
+                    if (current.right < find.left) {
+                        if (!most.left || dist < most.left.dist) {
+                            most.left = {
+                                dist: dist,
+                                node: current.node
+                            };
+                        }
+                    }
+                    // right check
+                    if (current.left > find.right) {
+                        if (!most.right || dist < most.right.dist) {
+                            most.right = {
+                                dist: dist,
+                                node: current.node
+                            };
+                        }
+                    }
+                    // top check
+                    if (current.bottom < find.top) {
+                        if (!most.top || dist < most.top.dist) {
+                            most.top = {
+                                dist: dist,
+                                node: current.node
+                            };
+                        }
+                    }
+                    // bottom check
+                    if (current.top > find.bottom) {
+                        if (!most.down || dist < most.down.dist) {
+                            most.down = {
+                                dist: dist,
+                                node: current.node
+                            };
+                        }
+                    }
+                }
+                find.node._nearestNodes = {
+                    right: most.right && most.right.node || null,
+                    top: most.top && most.top.node || null,
+                    left: most.left && most.left.node || null,
+                    down: most.down && most.down.node || null
+                };
+            }
+            function navigateTo(km, direction) {
+                var referNode = km.getSelectedNode();
+                if (!referNode) {
+                    km.select(km.getRoot());
+                    buildPositionNetwork(km.getRoot());
+                    return;
+                }
+                if (!referNode._nearestNodes) {
+                    buildPositionNetwork(km.getRoot());
+                }
+                var nextNode = referNode._nearestNodes[direction];
+                if (nextNode) {
+                    km.select(nextNode, true);
+                }
+            }
+            // 稀释用
+            var lastFrame;
+            return {
+                events: {
+                    layoutallfinish: function() {
+                        var root = this.getRoot();
+                        buildPositionNetwork(root);
+                    },
+                    "normal.keydown readonly.keydown": function(e) {
+                        var minder = this;
+                        [ "left", "right", "up", "down" ].forEach(function(key) {
+                            if (e.isShortcutKey(key)) {
+                                navigateTo(minder, key == "up" ? "top" : key);
+                                e.preventDefault();
+                            }
+                        });
+                    }
+                }
+            };
+        });
+    }
+};
+
+//src/module/layout.js
+/**
+ * @fileOverview
+ *
+ * 布局模块
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[52] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        /**
+     * @command Layout
+     * @description 设置选中节点的布局
+     *     允许使用的布局可以使用 `kityminder.Minder.getLayoutList()` 查询
+     * @param {string} name 布局的名称,设置为 null 则使用继承或默认的布局
+     * @state
+     *   0: 当前有选中的节点
+     *  -1: 当前没有选中的节点
+     * @return 返回首个选中节点的布局名称
+     */
+        var LayoutCommand = kity.createClass("LayoutCommand", {
+            base: Command,
+            execute: function(minder, name) {
+                var nodes = minder.getSelectedNodes();
+                nodes.forEach(function(node) {
+                    node.layout(name);
+                });
+            },
+            queryValue: function(minder) {
+                var node = minder.getSelectedNode();
+                if (node) {
+                    return node.getData("layout");
+                }
+            },
+            queryState: function(minder) {
+                return minder.getSelectedNode() ? 0 : -1;
+            }
+        });
+        /**
+     * @command ResetLayout
+     * @description 重设选中节点的布局,如果当前没有选中的节点,重设整个脑图的布局
+     * @state
+     *   0: 始终可用
+     * @return 返回首个选中节点的布局名称
+     */
+        var ResetLayoutCommand = kity.createClass("ResetLayoutCommand", {
+            base: Command,
+            execute: function(minder) {
+                var nodes = minder.getSelectedNodes();
+                if (!nodes.length) nodes = [ minder.getRoot() ];
+                nodes.forEach(function(node) {
+                    node.traverse(function(child) {
+                        child.resetLayoutOffset();
+                        if (!child.isRoot()) {
+                            child.setData("layout", null);
+                        }
+                    });
+                });
+                minder.layout(300);
+            },
+            enableReadOnly: true
+        });
+        Module.register("LayoutModule", {
+            commands: {
+                layout: LayoutCommand,
+                resetlayout: ResetLayoutCommand
+            },
+            contextmenu: [ {
+                command: "resetlayout"
+            }, {
+                divider: true
+            } ],
+            commandShortcutKeys: {
+                resetlayout: "Ctrl+Shift+L"
+            }
+        });
+    }
+};
+
+//src/module/node.js
+_p[53] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        /**
+     * @command AppendChildNode
+     * @description 添加子节点到选中的节点中
+     * @param {string|object} textOrData 要插入的节点的文本或数据
+     * @state
+     *    0: 当前有选中的节点
+     *   -1: 当前没有选中的节点
+     */
+        var AppendChildCommand = kity.createClass("AppendChildCommand", {
+            base: Command,
+            execute: function(km, text) {
+                var parent = km.getSelectedNode();
+                if (!parent) {
+                    return null;
+                }
+                var node = km.createNode(text, parent);
+                km.select(node, true);
+                if (parent.isExpanded()) {
+                    node.render();
+                } else {
+                    parent.expand();
+                    parent.renderTree();
+                }
+                km.layout(600);
+            },
+            queryState: function(km) {
+                var selectedNode = km.getSelectedNode();
+                return selectedNode ? 0 : -1;
+            }
+        });
+        /**
+     * @command AppendSiblingNode
+     * @description 添加选中的节点的兄弟节点
+     * @param {string|object} textOrData 要添加的节点的文本或数据
+     * @state
+     *    0: 当前有选中的节点
+     *   -1: 当前没有选中的节点
+     */
+        var AppendSiblingCommand = kity.createClass("AppendSiblingCommand", {
+            base: Command,
+            execute: function(km, text) {
+                var sibling = km.getSelectedNode();
+                var parent = sibling.parent;
+                if (!parent) {
+                    return km.execCommand("AppendChildNode", text);
+                }
+                var node = km.createNode(text, parent, sibling.getIndex() + 1);
+                node.setGlobalLayoutTransform(sibling.getGlobalLayoutTransform());
+                km.select(node, true);
+                node.render();
+                km.layout(600);
+            },
+            queryState: function(km) {
+                var selectedNode = km.getSelectedNode();
+                return selectedNode ? 0 : -1;
+            }
+        });
+        /**
+     * @command RemoveNode
+     * @description 移除选中的节点
+     * @state
+     *    0: 当前有选中的节点
+     *   -1: 当前没有选中的节点
+     */
+        var RemoveNodeCommand = kity.createClass("RemoverNodeCommand", {
+            base: Command,
+            execute: function(km) {
+                var nodes = km.getSelectedNodes();
+                var ancestor = MinderNode.getCommonAncestor.apply(null, nodes);
+                var index = nodes[0].getIndex();
+                nodes.forEach(function(node) {
+                    if (!node.isRoot()) km.removeNode(node);
+                });
+                if (nodes.length == 1) {
+                    var selectBack = ancestor.children[index - 1] || ancestor.children[index];
+                    km.select(selectBack || ancestor || km.getRoot(), true);
+                } else {
+                    km.select(ancestor || km.getRoot(), true);
+                }
+                km.layout(600);
+            },
+            queryState: function(km) {
+                var selectedNode = km.getSelectedNode();
+                return selectedNode && !selectedNode.isRoot() ? 0 : -1;
+            }
+        });
+        var AppendParentCommand = kity.createClass("AppendParentCommand", {
+            base: Command,
+            execute: function(km, text) {
+                var nodes = km.getSelectedNodes();
+                nodes.sort(function(a, b) {
+                    return a.getIndex() - b.getIndex();
+                });
+                var parent = nodes[0].parent;
+                var newParent = km.createNode(text, parent, nodes[0].getIndex());
+                nodes.forEach(function(node) {
+                    newParent.appendChild(node);
+                });
+                newParent.setGlobalLayoutTransform(nodes[nodes.length >> 1].getGlobalLayoutTransform());
+                km.select(newParent, true);
+                km.layout(600);
+            },
+            queryState: function(km) {
+                var nodes = km.getSelectedNodes();
+                if (!nodes.length) return -1;
+                var parent = nodes[0].parent;
+                if (!parent) return -1;
+                for (var i = 1; i < nodes.length; i++) {
+                    if (nodes[i].parent != parent) return -1;
+                }
+                return 0;
+            }
+        });
+        Module.register("NodeModule", function() {
+            return {
+                commands: {
+                    AppendChildNode: AppendChildCommand,
+                    AppendSiblingNode: AppendSiblingCommand,
+                    RemoveNode: RemoveNodeCommand,
+                    AppendParentNode: AppendParentCommand
+                },
+                commandShortcutKeys: {
+                    appendsiblingnode: "normal::Enter",
+                    appendchildnode: "normal::Insert|Tab",
+                    appendparentnode: "normal::Shift+Tab|normal::Shift+Insert",
+                    removenode: "normal::Del|Backspace"
+                }
+            };
+        });
+    }
+};
+
+//src/module/note.js
+/**
+ * @fileOverview
+ *
+ * 支持节点详细信息(HTML)格式
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[54] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("NoteModule", function() {
+            var NOTE_PATH = "M9,9H3V8h6L9,9L9,9z M9,7H3V6h6V7z M9,5H3V4h6V5z M8.5,11H2V2h8v7.5 M9,12l2-2V1H1v11";
+            /**
+         * @command Note
+         * @description 设置节点的备注信息
+         * @param {string} note 要设置的备注信息,设置为 null 则移除备注信息
+         * @state
+         *    0: 当前有选中的节点
+         *   -1: 当前没有选中的节点
+         */
+            var NoteCommand = kity.createClass("NoteCommand", {
+                base: Command,
+                execute: function(minder, note) {
+                    var node = minder.getSelectedNode();
+                    node.setData("note", note);
+                    node.render();
+                    node.getMinder().layout(300);
+                },
+                queryState: function(minder) {
+                    return minder.getSelectedNodes().length === 1 ? 0 : -1;
+                },
+                queryValue: function(minder) {
+                    var node = minder.getSelectedNode();
+                    return node && node.getData("note");
+                }
+            });
+            var NoteIcon = kity.createClass("NoteIcon", {
+                base: kity.Group,
+                constructor: function() {
+                    this.callBase();
+                    this.width = 16;
+                    this.height = 17;
+                    this.rect = new kity.Rect(16, 17, .5, -8.5, 2).fill("transparent");
+                    this.path = new kity.Path().setPathData(NOTE_PATH).setTranslate(2.5, -6.5);
+                    this.addShapes([ this.rect, this.path ]);
+                    this.on("mouseover", function() {
+                        this.rect.fill("rgba(255, 255, 200, .8)");
+                    }).on("mouseout", function() {
+                        this.rect.fill("transparent");
+                    });
+                    this.setStyle("cursor", "pointer");
+                }
+            });
+            var NoteIconRenderer = kity.createClass("NoteIconRenderer", {
+                base: Renderer,
+                create: function(node) {
+                    var icon = new NoteIcon();
+                    icon.on("mousedown", function(e) {
+                        e.preventDefault();
+                        node.getMinder().fire("editnoterequest");
+                    });
+                    icon.on("mouseover", function() {
+                        node.getMinder().fire("shownoterequest", {
+                            node: node,
+                            icon: icon
+                        });
+                    });
+                    icon.on("mouseout", function() {
+                        node.getMinder().fire("hidenoterequest", {
+                            node: node,
+                            icon: icon
+                        });
+                    });
+                    return icon;
+                },
+                shouldRender: function(node) {
+                    return node.getData("note");
+                },
+                update: function(icon, node, box) {
+                    var x = box.right + node.getStyle("space-left");
+                    var y = box.cy;
+                    icon.path.fill(node.getStyle("color"));
+                    icon.setTranslate(x, y);
+                    return new kity.Box(x, Math.round(y - icon.height / 2), icon.width, icon.height);
+                }
+            });
+            return {
+                renderers: {
+                    right: NoteIconRenderer
+                },
+                commands: {
+                    note: NoteCommand
+                }
+            };
+        });
+    }
+};
+
+//src/module/outline.js
+_p[55] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        var OutlineRenderer = kity.createClass("OutlineRenderer", {
+            base: Renderer,
+            create: function(node) {
+                var outline = new kity.Rect().setId(utils.uuid("node_outline"));
+                this.bringToBack = true;
+                return outline;
+            },
+            update: function(outline, node, box) {
+                var shape = node.getStyle("shape");
+                var paddingLeft = node.getStyle("padding-left"), paddingRight = node.getStyle("padding-right"), paddingTop = node.getStyle("padding-top"), paddingBottom = node.getStyle("padding-bottom");
+                var outlineBox = {
+                    x: box.x - paddingLeft,
+                    y: box.y - paddingTop,
+                    width: box.width + paddingLeft + paddingRight,
+                    height: box.height + paddingTop + paddingBottom
+                };
+                var radius = node.getStyle("radius");
+                // 天盘图圆形的情况
+                if (shape && shape == "circle") {
+                    var p = Math.pow;
+                    var r = Math.round;
+                    radius = r(Math.sqrt(p(outlineBox.width, 2) + p(outlineBox.height, 2)) / 2);
+                    outlineBox.x = box.cx - radius;
+                    outlineBox.y = box.cy - radius;
+                    outlineBox.width = 2 * radius;
+                    outlineBox.height = 2 * radius;
+                }
+                var prefix = node.isSelected() ? node.getMinder().isFocused() ? "selected-" : "blur-selected-" : "";
+                outline.setPosition(outlineBox.x, outlineBox.y).setSize(outlineBox.width, outlineBox.height).setRadius(radius).fill(node.getData("background") || node.getStyle(prefix + "background") || node.getStyle("background")).stroke(node.getStyle(prefix + "stroke" || node.getStyle("stroke")), node.getStyle(prefix + "stroke-width"));
+                return new kity.Box(outlineBox);
+            }
+        });
+        var ShadowRenderer = kity.createClass("ShadowRenderer", {
+            base: Renderer,
+            create: function(node) {
+                this.bringToBack = true;
+                return new kity.Rect();
+            },
+            shouldRender: function(node) {
+                return node.getStyle("shadow");
+            },
+            update: function(shadow, node, box) {
+                shadow.setPosition(box.x + 4, box.y + 5).fill(node.getStyle("shadow"));
+                var shape = node.getStyle("shape");
+                if (!shape) {
+                    shadow.setSize(box.width, box.height);
+                    shadow.setRadius(node.getStyle("radius"));
+                } else if (shape == "circle") {
+                    var width = Math.max(box.width, box.height);
+                    shadow.setSize(width, width);
+                    shadow.setRadius(width / 2);
+                }
+            }
+        });
+        var marker = new kity.Marker();
+        marker.setWidth(10);
+        marker.setHeight(12);
+        marker.setRef(0, 0);
+        marker.setViewBox(-6, -4, 8, 10);
+        marker.addShape(new kity.Path().setPathData("M-5-3l5,3,-5,3").stroke("#33ffff"));
+        var wireframeOption = /wire/.test(window.location.href);
+        var WireframeRenderer = kity.createClass("WireframeRenderer", {
+            base: Renderer,
+            create: function() {
+                var wireframe = new kity.Group();
+                var oxy = this.oxy = new kity.Path().stroke("#f6f").setPathData("M0,-50L0,50M-50,0L50,0");
+                var box = this.wireframe = new kity.Rect().stroke("lightgreen");
+                var vectorIn = this.vectorIn = new kity.Path().stroke("#66ffff");
+                var vectorOut = this.vectorOut = new kity.Path().stroke("#66ffff");
+                vectorIn.setMarker(marker, "end");
+                vectorOut.setMarker(marker, "end");
+                return wireframe.addShapes([ oxy, box, vectorIn, vectorOut ]);
+            },
+            shouldRender: function() {
+                return wireframeOption;
+            },
+            update: function(created, node, box) {
+                this.wireframe.setPosition(box.x, box.y).setSize(box.width, box.height);
+                var pin = node.getVertexIn();
+                var pout = node.getVertexOut();
+                var vin = node.getLayoutVectorIn().normalize(30);
+                var vout = node.getLayoutVectorOut().normalize(30);
+                this.vectorIn.setPathData([ "M", pin.offset(vin.reverse()), "L", pin ]);
+                this.vectorOut.setPathData([ "M", pout, "l", vout ]);
+            }
+        });
+        Module.register("OutlineModule", function() {
+            return {
+                events: !wireframeOption ? null : {
+                    ready: function() {
+                        this.getPaper().addResource(marker);
+                    },
+                    layoutallfinish: function() {
+                        this.getRoot().traverse(function(node) {
+                            node.getRenderer("WireframeRenderer").update(null, node, node.getContentBox());
+                        });
+                    }
+                },
+                renderers: {
+                    outline: OutlineRenderer,
+                    outside: [ ShadowRenderer, WireframeRenderer ]
+                }
+            };
+        });
+    }
+};
+
+//src/module/priority.js
+_p[56] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("PriorityModule", function() {
+            var minder = this;
+            // Designed by Akikonata
+            // [MASK, BACK]
+            var PRIORITY_COLORS = [ null, [ "#FF1200", "#840023" ], // 1 - red
+            [ "#0074FF", "#01467F" ], // 2 - blue
+            [ "#00AF00", "#006300" ], // 3 - green
+            [ "#FF962E", "#B25000" ], // 4 - orange
+            [ "#A464FF", "#4720C4" ], // 5 - purple
+            [ "#A3A3A3", "#515151" ], // 6,7,8,9 - gray
+            [ "#A3A3A3", "#515151" ], [ "#A3A3A3", "#515151" ], [ "#A3A3A3", "#515151" ] ];
+            // hue from 1 to 5
+            // jscs:disable maximumLineLength
+            var BACK_PATH = "M0,13c0,3.866,3.134,7,7,7h6c3.866,0,7-3.134,7-7V7H0V13z";
+            var MASK_PATH = "M20,10c0,3.866-3.134,7-7,7H7c-3.866,0-7-3.134-7-7V7c0-3.866,3.134-7,7-7h6c3.866,0,7,3.134,7,7V10z";
+            var PRIORITY_DATA = "priority";
+            // 优先级图标的图形
+            var PriorityIcon = kity.createClass("PriorityIcon", {
+                base: kity.Group,
+                constructor: function() {
+                    this.callBase();
+                    this.setSize(20);
+                    this.create();
+                    this.setId(utils.uuid("node_priority"));
+                },
+                setSize: function(size) {
+                    this.width = this.height = size;
+                },
+                create: function() {
+                    var white, back, mask, number;
+                    // 4 layer
+                    white = new kity.Path().setPathData(MASK_PATH).fill("white");
+                    back = new kity.Path().setPathData(BACK_PATH).setTranslate(.5, .5);
+                    mask = new kity.Path().setPathData(MASK_PATH).setOpacity(.8).setTranslate(.5, .5);
+                    number = new kity.Text().setX(this.width / 2 - .5).setY(this.height / 2).setTextAnchor("middle").setVerticalAlign("middle").setFontItalic(true).setFontSize(12).fill("white");
+                    this.addShapes([ back, mask, number ]);
+                    this.mask = mask;
+                    this.back = back;
+                    this.number = number;
+                },
+                setValue: function(value) {
+                    var back = this.back, mask = this.mask, number = this.number;
+                    var color = PRIORITY_COLORS[value];
+                    if (color) {
+                        back.fill(color[1]);
+                        mask.fill(color[0]);
+                    }
+                    number.setContent(value);
+                }
+            });
+            /**
+         * @command Priority
+         * @description 设置节点的优先级信息
+         * @param {number} value 要设置的优先级(添加一个优先级小图标)
+         *     取值为 0 移除优先级信息;
+         *     取值为 1 - 9 设置优先级,超过 9 的优先级不渲染
+         * @state
+         *    0: 当前有选中的节点
+         *   -1: 当前没有选中的节点
+         */
+            var PriorityCommand = kity.createClass("SetPriorityCommand", {
+                base: Command,
+                execute: function(km, value) {
+                    var nodes = km.getSelectedNodes();
+                    for (var i = 0; i < nodes.length; i++) {
+                        nodes[i].setData(PRIORITY_DATA, value || null).render();
+                    }
+                    km.layout();
+                },
+                queryValue: function(km) {
+                    var nodes = km.getSelectedNodes();
+                    var val;
+                    for (var i = 0; i < nodes.length; i++) {
+                        val = nodes[i].getData(PRIORITY_DATA);
+                        if (val) break;
+                    }
+                    return val || null;
+                },
+                queryState: function(km) {
+                    return km.getSelectedNodes().length ? 0 : -1;
+                }
+            });
+            return {
+                commands: {
+                    priority: PriorityCommand
+                },
+                renderers: {
+                    left: kity.createClass("PriorityRenderer", {
+                        base: Renderer,
+                        create: function(node) {
+                            return new PriorityIcon();
+                        },
+                        shouldRender: function(node) {
+                            return node.getData(PRIORITY_DATA);
+                        },
+                        update: function(icon, node, box) {
+                            var data = node.getData(PRIORITY_DATA);
+                            var spaceLeft = node.getStyle("space-left"), x, y;
+                            icon.setValue(data);
+                            x = box.left - icon.width - spaceLeft;
+                            y = -icon.height / 2;
+                            icon.setTranslate(x, y);
+                            return new kity.Box({
+                                x: x,
+                                y: y,
+                                width: icon.width,
+                                height: icon.height
+                            });
+                        }
+                    })
+                }
+            };
+        });
+    }
+};
+
+//src/module/progress.js
+_p[57] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("ProgressModule", function() {
+            var minder = this;
+            var PROGRESS_DATA = "progress";
+            // Designed by Akikonata
+            var BG_COLOR = "#FFED83";
+            var PIE_COLOR = "#43BC00";
+            var SHADOW_PATH = "M10,3c4.418,0,8,3.582,8,8h1c0-5.523-3.477-10-9-10S1,5.477,1,11h1C2,6.582,5.582,3,10,3z";
+            var SHADOW_COLOR = "#8E8E8E";
+            // jscs:disable maximumLineLength
+            var FRAME_PATH = "M10,0C4.477,0,0,4.477,0,10c0,5.523,4.477,10,10,10s10-4.477,10-10C20,4.477,15.523,0,10,0zM10,18c-4.418,0-8-3.582-8-8s3.582-8,8-8s8,3.582,8,8S14.418,18,10,18z";
+            var FRAME_GRAD = new kity.LinearGradient().pipe(function(g) {
+                g.setStartPosition(0, 0);
+                g.setEndPosition(0, 1);
+                g.addStop(0, "#fff");
+                g.addStop(1, "#ccc");
+            });
+            var CHECK_PATH = "M15.812,7.896l-6.75,6.75l-4.5-4.5L6.25,8.459l2.812,2.803l5.062-5.053L15.812,7.896z";
+            var CHECK_COLOR = "#EEE";
+            minder.getPaper().addResource(FRAME_GRAD);
+            // 进度图标的图形
+            var ProgressIcon = kity.createClass("ProgressIcon", {
+                base: kity.Group,
+                constructor: function(value) {
+                    this.callBase();
+                    this.setSize(20);
+                    this.create();
+                    this.setValue(value);
+                    this.setId(utils.uuid("node_progress"));
+                    this.translate(.5, .5);
+                },
+                setSize: function(size) {
+                    this.width = this.height = size;
+                },
+                create: function() {
+                    var bg, pie, shadow, frame, check;
+                    bg = new kity.Circle(9).fill(BG_COLOR);
+                    pie = new kity.Pie(9, 0).fill(PIE_COLOR);
+                    shadow = new kity.Path().setPathData(SHADOW_PATH).setTranslate(-10, -10).fill(SHADOW_COLOR);
+                    frame = new kity.Path().setTranslate(-10, -10).setPathData(FRAME_PATH).fill(FRAME_GRAD);
+                    check = new kity.Path().setTranslate(-10, -10).setPathData(CHECK_PATH).fill(CHECK_COLOR);
+                    this.addShapes([ bg, pie, shadow, check, frame ]);
+                    this.pie = pie;
+                    this.check = check;
+                },
+                setValue: function(value) {
+                    this.pie.setAngle(-360 * (value - 1) / 8);
+                    this.check.setVisible(value == 9);
+                }
+            });
+            /**
+         * @command Progress
+         * @description 设置节点的进度信息(添加一个进度小图标)
+         * @param {number} value 要设置的进度
+         *     取值为 0 移除进度信息;
+         *     取值为 1 表示未开始;
+         *     取值为 2 表示完成 1/8;
+         *     取值为 3 表示完成 2/8;
+         *     取值为 4 表示完成 3/8;
+         *     其余类推,取值为 9 表示全部完成
+         * @state
+         *    0: 当前有选中的节点
+         *   -1: 当前没有选中的节点
+         */
+            var ProgressCommand = kity.createClass("ProgressCommand", {
+                base: Command,
+                execute: function(km, value) {
+                    var nodes = km.getSelectedNodes();
+                    for (var i = 0; i < nodes.length; i++) {
+                        nodes[i].setData(PROGRESS_DATA, value || null).render();
+                    }
+                    km.layout();
+                },
+                queryValue: function(km) {
+                    var nodes = km.getSelectedNodes();
+                    var val;
+                    for (var i = 0; i < nodes.length; i++) {
+                        val = nodes[i].getData(PROGRESS_DATA);
+                        if (val) break;
+                    }
+                    return val || null;
+                },
+                queryState: function(km) {
+                    return km.getSelectedNodes().length ? 0 : -1;
+                }
+            });
+            return {
+                commands: {
+                    progress: ProgressCommand
+                },
+                renderers: {
+                    left: kity.createClass("ProgressRenderer", {
+                        base: Renderer,
+                        create: function(node) {
+                            return new ProgressIcon();
+                        },
+                        shouldRender: function(node) {
+                            return node.getData(PROGRESS_DATA);
+                        },
+                        update: function(icon, node, box) {
+                            var data = node.getData(PROGRESS_DATA);
+                            var spaceLeft = node.getStyle("space-left");
+                            var x, y;
+                            icon.setValue(data);
+                            x = box.left - icon.width - spaceLeft;
+                            y = -icon.height / 2;
+                            icon.setTranslate(x + icon.width / 2, y + icon.height / 2);
+                            return new kity.Box(x, y, icon.width, icon.height);
+                        }
+                    })
+                }
+            };
+        });
+    }
+};
+
+//src/module/resource.js
+_p[58] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("Resource", function() {
+            // String Hash
+            // https://github.com/drostie/sha3-js/edit/master/blake32.min.js
+            var blake32 = function() {
+                var k, g, r, l, m, o, p, q, t, w, x;
+                x = 4 * (1 << 30);
+                k = [ 1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225 ];
+                m = [ 608135816, 2242054355, 320440878, 57701188, 2752067618, 698298832, 137296536, 3964562569, 1160258022, 953160567, 3193202383, 887688300, 3232508343, 3380367581, 1065670069, 3041331479 ];
+                w = function(i) {
+                    if (i < 0) {
+                        i += x;
+                    }
+                    return ("00000000" + i.toString(16)).slice(-8);
+                };
+                o = [ [ 16, 50, 84, 118, 152, 186, 220, 254 ], [ 174, 132, 249, 109, 193, 32, 123, 53 ], [ 139, 12, 37, 223, 234, 99, 23, 73 ], [ 151, 19, 205, 235, 98, 165, 4, 143 ], [ 9, 117, 66, 250, 30, 203, 134, 211 ], [ 194, 166, 176, 56, 212, 87, 239, 145 ], [ 92, 241, 222, 164, 112, 54, 41, 184 ], [ 189, 231, 28, 147, 5, 79, 104, 162 ], [ 246, 158, 59, 128, 44, 125, 65, 90 ], [ 42, 72, 103, 81, 191, 233, 195, 13 ] ];
+                p = function(a, b, n) {
+                    var s = q[a] ^ q[b];
+                    q[a] = s >>> n | s << 32 - n;
+                };
+                g = function(i, a, b, c, d) {
+                    var u = l + o[r][i] % 16, v = l + (o[r][i] >> 4);
+                    a %= 4;
+                    b = 4 + b % 4;
+                    c = 8 + c % 4;
+                    d = 12 + d % 4;
+                    q[a] += q[b] + (t[u] ^ m[v % 16]);
+                    p(d, a, 16);
+                    q[c] += q[d];
+                    p(b, c, 12);
+                    q[a] += q[b] + (t[v] ^ m[u % 16]);
+                    p(d, a, 8);
+                    q[c] += q[d];
+                    p(b, c, 7);
+                };
+                return function(a, b) {
+                    if (!(b instanceof Array && b.length === 4)) {
+                        b = [ 0, 0, 0, 0 ];
+                    }
+                    var c, d, e, L, f, h, j, i;
+                    d = k.slice(0);
+                    c = m.slice(0, 8);
+                    for (r = 0; r < 4; r += 1) {
+                        c[r] ^= b[r];
+                    }
+                    e = a.length * 16;
+                    f = e % 512 > 446 || e % 512 === 0 ? 0 : e;
+                    if (e % 512 === 432) {
+                        a += "老";
+                    } else {
+                        a += "耀";
+                        while (a.length % 32 !== 27) {
+                            a += "\0";
+                        }
+                        a += "";
+                    }
+                    t = [];
+                    for (i = 0; i < a.length; i += 2) {
+                        t.push(a.charCodeAt(i) * 65536 + a.charCodeAt(i + 1));
+                    }
+                    t.push(0);
+                    t.push(e);
+                    h = t.length - 16;
+                    j = 0;
+                    for (l = 0; l < t.length; l += 16) {
+                        j += 512;
+                        L = l === h ? f : Math.min(e, j);
+                        q = d.concat(c);
+                        q[12] ^= L;
+                        q[13] ^= L;
+                        for (r = 0; r < 10; r += 1) {
+                            for (i = 0; i < 8; i += 1) {
+                                if (i < 4) {
+                                    g(i, i, i, i, i);
+                                } else {
+                                    g(i, i, i + 1, i + 2, i + 3);
+                                }
+                            }
+                        }
+                        for (i = 0; i < 8; i += 1) {
+                            d[i] ^= b[i % 4] ^ q[i] ^ q[i + 8];
+                        }
+                    }
+                    return d.map(w).join("");
+                };
+            }();
+            /**
+         * 自动使用的颜色序列
+         */
+            var RESOURCE_COLOR_SERIES = [ 51, 303, 75, 200, 157, 0, 26, 254 ].map(function(h) {
+                return kity.Color.createHSL(h, 100, 85);
+            });
+            /**
+         * 在 Minder 上拓展一些关于资源的支持接口
+         */
+            kity.extendClass(Minder, {
+                /**
+             * 获取字符串的哈希值
+             *
+             * @param {String} str
+             * @return {Number} hashCode
+             */
+                getHashCode: function(str) {
+                    str = blake32(str);
+                    var hash = 1315423911, i, ch;
+                    for (i = str.length - 1; i >= 0; i--) {
+                        ch = str.charCodeAt(i);
+                        hash ^= (hash << 5) + ch + (hash >> 2);
+                    }
+                    return hash & 2147483647;
+                },
+                /**
+             * 获取脑图中某个资源对应的颜色
+             *
+             * 如果存在同名资源,则返回已经分配给该资源的颜色,否则分配给该资源一个颜色,并且返回
+             *
+             * 如果资源数超过颜色序列数量,返回哈希颜色
+             *
+             * @param {String} resource 资源名称
+             * @return {Color}
+             */
+                getResourceColor: function(resource) {
+                    var colorMapping = this._getResourceColorIndexMapping();
+                    var nextIndex;
+                    if (!Object.prototype.hasOwnProperty.call(colorMapping, resource)) {
+                        // 找不到找下个可用索引
+                        nextIndex = this._getNextResourceColorIndex();
+                        colorMapping[resource] = nextIndex;
+                    }
+                    // 资源过多,找不到可用索引颜色,统一返回哈希函数得到的颜色
+                    return RESOURCE_COLOR_SERIES[colorMapping[resource]] || kity.Color.createHSL(Math.floor(this.getHashCode(resource) / 2147483647 * 359), 100, 85);
+                },
+                /**
+             * 获得已使用的资源的列表
+             *
+             * @return {Array}
+             */
+                getUsedResource: function() {
+                    var mapping = this._getResourceColorIndexMapping();
+                    var used = [], resource;
+                    for (resource in mapping) {
+                        if (Object.prototype.hasOwnProperty.call(mapping, resource)) {
+                            used.push(resource);
+                        }
+                    }
+                    return used;
+                },
+                /**
+             * 获取脑图下一个可用的资源颜色索引
+             *
+             * @return {int}
+             */
+                _getNextResourceColorIndex: function() {
+                    // 获取现有颜色映射
+                    //     resource => color_index
+                    var colorMapping = this._getResourceColorIndexMapping();
+                    var resource, used, i;
+                    used = [];
+                    // 抽取已经使用的值到 used 数组
+                    for (resource in colorMapping) {
+                        if (Object.prototype.hasOwnProperty.call(colorMapping, resource)) {
+                            used.push(colorMapping[resource]);
+                        }
+                    }
+                    // 枚举所有的可用值,如果还没被使用,返回
+                    for (i = 0; i < RESOURCE_COLOR_SERIES.length; i++) {
+                        if (!~used.indexOf(i)) return i;
+                    }
+                    // 没有可用的颜色了
+                    return -1;
+                },
+                // 获取现有颜色映射
+                //     resource => color_index
+                _getResourceColorIndexMapping: function() {
+                    return this._resourceColorMapping || (this._resourceColorMapping = {});
+                }
+            });
+            /**
+         * @class 设置资源的命令
+         *
+         * @example
+         *
+         * // 设置选中节点资源为 "张三"
+         * minder.execCommand('resource', ['张三']);
+         *
+         * // 添加资源 "李四" 到选中节点
+         * var resource = minder.queryCommandValue();
+         * resource.push('李四');
+         * minder.execCommand('resource', resource);
+         *
+         * // 清除选中节点的资源
+         * minder.execCommand('resource', null);
+         */
+            var ResourceCommand = kity.createClass("ResourceCommand", {
+                base: Command,
+                execute: function(minder, resource) {
+                    var nodes = minder.getSelectedNodes();
+                    if (typeof resource == "string") {
+                        resource = [ resource ];
+                    }
+                    nodes.forEach(function(node) {
+                        node.setData("resource", resource).render();
+                    });
+                    minder.layout(200);
+                },
+                queryValue: function(minder) {
+                    var nodes = minder.getSelectedNodes();
+                    var resource = [];
+                    nodes.forEach(function(node) {
+                        var nodeResource = node.getData("resource");
+                        if (!nodeResource) return;
+                        nodeResource.forEach(function(name) {
+                            if (!~resource.indexOf(name)) {
+                                resource.push(name);
+                            }
+                        });
+                    });
+                    return resource;
+                },
+                queryState: function(km) {
+                    return km.getSelectedNode() ? 0 : -1;
+                }
+            });
+            /**
+         * @class 资源的覆盖图形
+         *
+         * 该类为一个资源以指定的颜色渲染一个动态的覆盖图形
+         */
+            var ResourceOverlay = kity.createClass("ResourceOverlay", {
+                base: kity.Group,
+                constructor: function() {
+                    this.callBase();
+                    var text, rect;
+                    rect = this.rect = new kity.Rect().setRadius(4);
+                    text = this.text = new kity.Text().setFontSize(12).setVerticalAlign("middle");
+                    this.addShapes([ rect, text ]);
+                },
+                setValue: function(resourceName, color) {
+                    var paddingX = 8, paddingY = 4, borderRadius = 4;
+                    var text, box, rect;
+                    text = this.text;
+                    if (resourceName == this.lastResourceName) {
+                        box = this.lastBox;
+                    } else {
+                        text.setContent(resourceName);
+                        box = text.getBoundaryBox();
+                        this.lastResourceName = resourceName;
+                        this.lastBox = box;
+                    }
+                    text.setX(paddingX).fill(color.dec("l", 70));
+                    rect = this.rect;
+                    rect.setPosition(0, box.y - paddingY);
+                    this.width = Math.round(box.width + paddingX * 2);
+                    this.height = Math.round(box.height + paddingY * 2);
+                    rect.setSize(this.width, this.height);
+                    rect.fill(color);
+                }
+            });
+            /**
+         * @class 资源渲染器
+         */
+            var ResourceRenderer = kity.createClass("ResourceRenderer", {
+                base: Renderer,
+                create: function(node) {
+                    this.overlays = [];
+                    return new kity.Group();
+                },
+                shouldRender: function(node) {
+                    return node.getData("resource") && node.getData("resource").length;
+                },
+                update: function(container, node, box) {
+                    var spaceRight = node.getStyle("space-right");
+                    var overlays = this.overlays;
+                    /*  修复 resource 数组中出现 null 的 bug
+                 *  @Author zhangbobell
+                 *  @date 2016-01-15
+                 */
+                    var resource = node.getData("resource").filter(function(ele) {
+                        return ele !== null;
+                    });
+                    if (resource.length === 0) {
+                        return;
+                    }
+                    var minder = node.getMinder();
+                    var i, overlay, x;
+                    x = 0;
+                    for (i = 0; i < resource.length; i++) {
+                        x += spaceRight;
+                        overlay = overlays[i];
+                        if (!overlay) {
+                            overlay = new ResourceOverlay();
+                            overlays.push(overlay);
+                            container.addShape(overlay);
+                        }
+                        overlay.setVisible(true);
+                        overlay.setValue(resource[i], minder.getResourceColor(resource[i]));
+                        overlay.setTranslate(x, -1);
+                        x += overlay.width;
+                    }
+                    while (overlay = overlays[i++]) overlay.setVisible(false);
+                    container.setTranslate(box.right, 0);
+                    return new kity.Box({
+                        x: box.right,
+                        y: Math.round(-overlays[0].height / 2),
+                        width: x,
+                        height: overlays[0].height
+                    });
+                }
+            });
+            return {
+                commands: {
+                    resource: ResourceCommand
+                },
+                renderers: {
+                    right: ResourceRenderer
+                }
+            };
+        });
+    }
+};
+
+//src/module/select.js
+_p[59] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("Select", function() {
+            var minder = this;
+            var rc = minder.getRenderContainer();
+            // 在实例上渲染框选矩形、计算框选范围的对象
+            var marqueeActivator = function() {
+                // 记录选区的开始位置(mousedown的位置)
+                var startPosition = null;
+                // 选区的图形
+                var marqueeShape = new kity.Path();
+                // 标记是否已经启动框选状态
+                //    并不是 mousedown 发生之后就启动框选状态,而是检测到移动了一定的距离(MARQUEE_MODE_THRESHOLD)之后
+                var marqueeMode = false;
+                var MARQUEE_MODE_THRESHOLD = 10;
+                return {
+                    selectStart: function(e) {
+                        // 只接受左键
+                        if (e.originEvent.button || e.originEvent.altKey) return;
+                        // 清理不正确状态
+                        if (startPosition) {
+                            return this.selectEnd();
+                        }
+                        startPosition = e.getPosition(rc).round();
+                    },
+                    selectMove: function(e) {
+                        if (minder.getStatus() == "textedit") {
+                            return;
+                        }
+                        if (!startPosition) return;
+                        var p1 = startPosition, p2 = e.getPosition(rc);
+                        // 检测是否要进入选区模式
+                        if (!marqueeMode) {
+                            // 距离没达到阈值,退出
+                            if (kity.Vector.fromPoints(p1, p2).length() < MARQUEE_MODE_THRESHOLD) {
+                                return;
+                            }
+                            // 已经达到阈值,记录下来并且重置选区形状
+                            marqueeMode = true;
+                            rc.addShape(marqueeShape);
+                            marqueeShape.fill(minder.getStyle("marquee-background")).stroke(minder.getStyle("marquee-stroke")).setOpacity(.8).getDrawer().clear();
+                        }
+                        var marquee = new kity.Box(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y), selectedNodes = [];
+                        // 使其犀利
+                        marquee.left = Math.round(marquee.left);
+                        marquee.top = Math.round(marquee.top);
+                        marquee.right = Math.round(marquee.right);
+                        marquee.bottom = Math.round(marquee.bottom);
+                        // 选区形状更新
+                        marqueeShape.getDrawer().pipe(function() {
+                            this.clear();
+                            this.moveTo(marquee.left, marquee.top);
+                            this.lineTo(marquee.right, marquee.top);
+                            this.lineTo(marquee.right, marquee.bottom);
+                            this.lineTo(marquee.left, marquee.bottom);
+                            this.close();
+                        });
+                        // 计算选中范围
+                        minder.getRoot().traverse(function(node) {
+                            var renderBox = node.getLayoutBox();
+                            if (!renderBox.intersect(marquee).isEmpty()) {
+                                selectedNodes.push(node);
+                            }
+                        });
+                        // 应用选中范围
+                        minder.select(selectedNodes, true);
+                        // 清除多余的东西
+                        window.getSelection().removeAllRanges();
+                    },
+                    selectEnd: function(e) {
+                        if (startPosition) {
+                            startPosition = null;
+                        }
+                        if (marqueeMode) {
+                            marqueeShape.fadeOut(200, "ease", 0, function() {
+                                if (marqueeShape.remove) marqueeShape.remove();
+                            });
+                            marqueeMode = false;
+                        }
+                    }
+                };
+            }();
+            var lastDownNode = null, lastDownPosition = null;
+            return {
+                init: function() {
+                    window.addEventListener("mouseup", function() {
+                        marqueeActivator.selectEnd();
+                    });
+                },
+                events: {
+                    mousedown: function(e) {
+                        var downNode = e.getTargetNode();
+                        // 没有点中节点:
+                        //     清除选中状态,并且标记选区开始位置
+                        if (!downNode) {
+                            this.removeAllSelectedNodes();
+                            marqueeActivator.selectStart(e);
+                            this.setStatus("normal");
+                        } else if (e.isShortcutKey("Ctrl")) {
+                            this.toggleSelect(downNode);
+                        } else if (!downNode.isSelected()) {
+                            this.select(downNode, true);
+                        } else if (!this.isSingleSelect()) {
+                            lastDownNode = downNode;
+                            lastDownPosition = e.getPosition();
+                        }
+                    },
+                    mousemove: marqueeActivator.selectMove,
+                    mouseup: function(e) {
+                        var upNode = e.getTargetNode();
+                        // 如果 mouseup 发生在 lastDownNode 外,是无需理会的
+                        if (upNode && upNode == lastDownNode) {
+                            var upPosition = e.getPosition();
+                            var movement = kity.Vector.fromPoints(lastDownPosition, upPosition);
+                            if (movement.length() < 1) this.select(lastDownNode, true);
+                            lastDownNode = null;
+                        }
+                        // 清理一下选择状态
+                        marqueeActivator.selectEnd(e);
+                    },
+                    //全选操作
+                    "normal.keydown": function(e) {
+                        if (e.isShortcutKey("ctrl+a")) {
+                            var selectedNodes = [];
+                            this.getRoot().traverse(function(node) {
+                                selectedNodes.push(node);
+                            });
+                            this.select(selectedNodes, true);
+                            e.preventDefault();
+                        }
+                    }
+                }
+            };
+        });
+    }
+};
+
+//src/module/style.js
+_p[60] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("StyleModule", function() {
+            var styleNames = [ "font-size", "font-family", "font-weight", "font-style", "background", "color" ];
+            var styleClipBoard = null;
+            function hasStyle(node) {
+                var data = node.getData();
+                for (var i = 0; i < styleNames.length; i++) {
+                    if (styleNames[i] in data) return true;
+                }
+            }
+            return {
+                commands: {
+                    /**
+                 * @command CopyStyle
+                 * @description 拷贝选中节点的当前样式,包括字体、字号、粗体、斜体、背景色、字体色
+                 * @state
+                 *   0: 当前有选中的节点
+                 *  -1: 当前没有选中的节点
+                 */
+                    copystyle: kity.createClass("CopyStyleCommand", {
+                        base: Command,
+                        execute: function(minder) {
+                            var node = minder.getSelectedNode();
+                            var nodeData = node.getData();
+                            styleClipBoard = {};
+                            styleNames.forEach(function(name) {
+                                if (name in nodeData) styleClipBoard[name] = nodeData[name]; else {
+                                    styleClipBoard[name] = null;
+                                    delete styleClipBoard[name];
+                                }
+                            });
+                            return styleClipBoard;
+                        },
+                        queryState: function(minder) {
+                            var nodes = minder.getSelectedNodes();
+                            if (nodes.length !== 1) return -1;
+                            return hasStyle(nodes[0]) ? 0 : -1;
+                        }
+                    }),
+                    /**
+                 * @command PasteStyle
+                 * @description 粘贴已拷贝的样式到选中的节点上,包括字体、字号、粗体、斜体、背景色、字体色
+                 * @state
+                 *   0: 当前有选中的节点,并且已经有复制的样式
+                 *  -1: 当前没有选中的节点,或者没有复制的样式
+                 */
+                    pastestyle: kity.createClass("PastStyleCommand", {
+                        base: Command,
+                        execute: function(minder) {
+                            minder.getSelectedNodes().forEach(function(node) {
+                                for (var name in styleClipBoard) {
+                                    if (styleClipBoard.hasOwnProperty(name)) node.setData(name, styleClipBoard[name]);
+                                }
+                            });
+                            minder.renderNodeBatch(minder.getSelectedNodes());
+                            minder.layout(300);
+                            return styleClipBoard;
+                        },
+                        queryState: function(minder) {
+                            return styleClipBoard && minder.getSelectedNodes().length ? 0 : -1;
+                        }
+                    }),
+                    /**
+                 * @command ClearStyle
+                 * @description 移除选中节点的样式,包括字体、字号、粗体、斜体、背景色、字体色
+                 * @state
+                 *   0: 当前有选中的节点,并且至少有一个设置了至少一种样式
+                 *  -1: 其它情况
+                 */
+                    clearstyle: kity.createClass("ClearStyleCommand", {
+                        base: Command,
+                        execute: function(minder) {
+                            minder.getSelectedNodes().forEach(function(node) {
+                                styleNames.forEach(function(name) {
+                                    node.setData(name);
+                                });
+                            });
+                            minder.renderNodeBatch(minder.getSelectedNodes());
+                            minder.layout(300);
+                            return styleClipBoard;
+                        },
+                        queryState: function(minder) {
+                            var nodes = minder.getSelectedNodes();
+                            if (!nodes.length) return -1;
+                            for (var i = 0; i < nodes.length; i++) {
+                                if (hasStyle(nodes[i])) return 0;
+                            }
+                            return -1;
+                        }
+                    })
+                }
+            };
+        });
+    }
+};
+
+//src/module/text.js
+_p[61] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        /**
+     * 针对不同系统、不同浏览器、不同字体做居中兼容性处理
+     * 暂时未增加Linux的处理
+     */
+        var FONT_ADJUST = {
+            safari: {
+                "微软雅黑,Microsoft YaHei": -.17,
+                "楷体,楷体_GB2312,SimKai": -.1,
+                "隶书, SimLi": -.1,
+                "comic sans ms": -.23,
+                "impact,chicago": -.15,
+                "times new roman": -.1,
+                "arial black,avant garde": -.17,
+                default: 0
+            },
+            ie: {
+                10: {
+                    "微软雅黑,Microsoft YaHei": -.17,
+                    "comic sans ms": -.17,
+                    "impact,chicago": -.08,
+                    "times new roman": .04,
+                    "arial black,avant garde": -.17,
+                    default: -.15
+                },
+                11: {
+                    "微软雅黑,Microsoft YaHei": -.17,
+                    "arial,helvetica,sans-serif": -.17,
+                    "comic sans ms": -.17,
+                    "impact,chicago": -.08,
+                    "times new roman": .04,
+                    "sans-serif": -.16,
+                    "arial black,avant garde": -.17,
+                    default: -.15
+                }
+            },
+            edge: {
+                "微软雅黑,Microsoft YaHei": -.15,
+                "arial,helvetica,sans-serif": -.17,
+                "comic sans ms": -.17,
+                "impact,chicago": -.08,
+                "sans-serif": -.16,
+                "arial black,avant garde": -.17,
+                default: -.15
+            },
+            sg: {
+                "微软雅黑,Microsoft YaHei": -.15,
+                "arial,helvetica,sans-serif": -.05,
+                "comic sans ms": -.22,
+                "impact,chicago": -.16,
+                "times new roman": -.03,
+                "arial black,avant garde": -.22,
+                default: -.15
+            },
+            chrome: {
+                Mac: {
+                    "andale mono": -.05,
+                    "comic sans ms": -.3,
+                    "impact,chicago": -.13,
+                    "times new roman": -.1,
+                    "arial black,avant garde": -.17,
+                    default: 0
+                },
+                Win: {
+                    "微软雅黑,Microsoft YaHei": -.15,
+                    "arial,helvetica,sans-serif": -.02,
+                    "arial black,avant garde": -.2,
+                    "comic sans ms": -.2,
+                    "impact,chicago": -.12,
+                    "times new roman": -.02,
+                    default: -.15
+                },
+                Lux: {
+                    "andale mono": -.05,
+                    "comic sans ms": -.3,
+                    "impact,chicago": -.13,
+                    "times new roman": -.1,
+                    "arial black,avant garde": -.17,
+                    default: 0
+                }
+            },
+            firefox: {
+                Mac: {
+                    "微软雅黑,Microsoft YaHei": -.2,
+                    "宋体,SimSun": .05,
+                    "comic sans ms": -.2,
+                    "impact,chicago": -.15,
+                    "arial black,avant garde": -.17,
+                    "times new roman": -.1,
+                    default: .05
+                },
+                Win: {
+                    "微软雅黑,Microsoft YaHei": -.16,
+                    "andale mono": -.17,
+                    "arial,helvetica,sans-serif": -.17,
+                    "comic sans ms": -.22,
+                    "impact,chicago": -.23,
+                    "times new roman": -.22,
+                    "sans-serif": -.22,
+                    "arial black,avant garde": -.17,
+                    default: -.16
+                },
+                Lux: {
+                    "宋体,SimSun": -.2,
+                    "微软雅黑,Microsoft YaHei": -.2,
+                    "黑体, SimHei": -.2,
+                    "隶书, SimLi": -.2,
+                    "楷体,楷体_GB2312,SimKai": -.2,
+                    "andale mono": -.2,
+                    "arial,helvetica,sans-serif": -.2,
+                    "comic sans ms": -.2,
+                    "impact,chicago": -.2,
+                    "times new roman": -.2,
+                    "sans-serif": -.2,
+                    "arial black,avant garde": -.2,
+                    default: -.16
+                }
+            }
+        };
+        var TextRenderer = kity.createClass("TextRenderer", {
+            base: Renderer,
+            create: function() {
+                return new kity.Group().setId(utils.uuid("node_text"));
+            },
+            update: function(textGroup, node) {
+                function getDataOrStyle(name) {
+                    return node.getData(name) || node.getStyle(name);
+                }
+                var nodeText = node.getText();
+                var textArr = nodeText ? nodeText.split("\n") : [ " " ];
+                var lineHeight = node.getStyle("line-height");
+                var fontSize = getDataOrStyle("font-size");
+                var fontFamily = getDataOrStyle("font-family") || "default";
+                var height = lineHeight * fontSize * textArr.length - (lineHeight - 1) * fontSize;
+                var yStart = -height / 2;
+                var Browser = kity.Browser;
+                var adjust;
+                if (Browser.chrome || Browser.opera || Browser.bd || Browser.lb === "chrome") {
+                    adjust = FONT_ADJUST["chrome"][Browser.platform][fontFamily];
+                } else if (Browser.gecko) {
+                    adjust = FONT_ADJUST["firefox"][Browser.platform][fontFamily];
+                } else if (Browser.sg) {
+                    adjust = FONT_ADJUST["sg"][fontFamily];
+                } else if (Browser.safari) {
+                    adjust = FONT_ADJUST["safari"][fontFamily];
+                } else if (Browser.ie) {
+                    adjust = FONT_ADJUST["ie"][Browser.version][fontFamily];
+                } else if (Browser.edge) {
+                    adjust = FONT_ADJUST["edge"][fontFamily];
+                } else if (Browser.lb) {
+                    // 猎豹浏览器的ie内核兼容性模式下
+                    adjust = .9;
+                }
+                textGroup.setTranslate(0, (adjust || 0) * fontSize);
+                var rBox = new kity.Box(), r = Math.round;
+                this.setTextStyle(node, textGroup);
+                var textLength = textArr.length;
+                var textGroupLength = textGroup.getItems().length;
+                var i, ci, textShape, text;
+                if (textLength < textGroupLength) {
+                    for (i = textLength, ci; ci = textGroup.getItem(i); ) {
+                        textGroup.removeItem(i);
+                    }
+                } else if (textLength > textGroupLength) {
+                    var growth = textLength - textGroupLength;
+                    while (growth--) {
+                        textShape = new kity.Text().setAttr("text-rendering", "inherit");
+                        if (kity.Browser.ie || kity.Browser.edge) {
+                            textShape.setVerticalAlign("top");
+                        } else {
+                            textShape.setAttr("dominant-baseline", "text-before-edge");
+                        }
+                        textGroup.addItem(textShape);
+                    }
+                }
+                for (i = 0, text, textShape; text = textArr[i], textShape = textGroup.getItem(i); i++) {
+                    textShape.setContent(text);
+                    if (kity.Browser.ie || kity.Browser.edge) {
+                        textShape.fixPosition();
+                    }
+                }
+                this.setTextStyle(node, textGroup);
+                var textHash = node.getText() + [ "font-size", "font-name", "font-weight", "font-style" ].map(getDataOrStyle).join("/");
+                if (node._currentTextHash == textHash && node._currentTextGroupBox) return node._currentTextGroupBox;
+                node._currentTextHash = textHash;
+                return function() {
+                    textGroup.eachItem(function(i, textShape) {
+                        var y = yStart + i * fontSize * lineHeight;
+                        textShape.setY(y);
+                        var bbox = textShape.getBoundaryBox();
+                        rBox = rBox.merge(new kity.Box(0, y, bbox.height && bbox.width || 1, fontSize));
+                    });
+                    var nBox = new kity.Box(r(rBox.x), r(rBox.y), r(rBox.width), r(rBox.height));
+                    node._currentTextGroupBox = nBox;
+                    return nBox;
+                };
+            },
+            setTextStyle: function(node, text) {
+                var hooks = TextRenderer._styleHooks;
+                hooks.forEach(function(hook) {
+                    hook(node, text);
+                });
+            }
+        });
+        var TextCommand = kity.createClass({
+            base: Command,
+            execute: function(minder, text) {
+                var node = minder.getSelectedNode();
+                if (node) {
+                    node.setText(text);
+                    node.render();
+                    minder.layout();
+                }
+            },
+            queryState: function(minder) {
+                return minder.getSelectedNodes().length == 1 ? 0 : -1;
+            },
+            queryValue: function(minder) {
+                var node = minder.getSelectedNode();
+                return node ? node.getText() : null;
+            }
+        });
+        utils.extend(TextRenderer, {
+            _styleHooks: [],
+            registerStyleHook: function(fn) {
+                TextRenderer._styleHooks.push(fn);
+            }
+        });
+        kity.extendClass(MinderNode, {
+            getTextGroup: function() {
+                return this.getRenderer("TextRenderer").getRenderShape();
+            }
+        });
+        Module.register("text", {
+            commands: {
+                text: TextCommand
+            },
+            renderers: {
+                center: TextRenderer
+            }
+        });
+        module.exports = TextRenderer;
+    }
+};
+
+//src/module/view.js
+_p[62] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        var ViewDragger = kity.createClass("ViewDragger", {
+            constructor: function(minder) {
+                this._minder = minder;
+                this._enabled = false;
+                this._bind();
+                var me = this;
+                this._minder.getViewDragger = function() {
+                    return me;
+                };
+                this.setEnabled(false);
+            },
+            isEnabled: function() {
+                return this._enabled;
+            },
+            setEnabled: function(value) {
+                var paper = this._minder.getPaper();
+                paper.setStyle("cursor", value ? "pointer" : "default");
+                paper.setStyle("cursor", value ? "-webkit-grab" : "default");
+                this._enabled = value;
+            },
+            timeline: function() {
+                return this._moveTimeline;
+            },
+            move: function(offset, duration) {
+                var minder = this._minder;
+                var targetPosition = this.getMovement().offset(offset);
+                this.moveTo(targetPosition, duration);
+            },
+            moveTo: function(position, duration) {
+                if (duration) {
+                    var dragger = this;
+                    if (this._moveTimeline) this._moveTimeline.stop();
+                    this._moveTimeline = this._minder.getRenderContainer().animate(new kity.Animator(this.getMovement(), position, function(target, value) {
+                        dragger.moveTo(value);
+                    }), duration, "easeOutCubic").timeline();
+                    this._moveTimeline.on("finish", function() {
+                        dragger._moveTimeline = null;
+                    });
+                    return this;
+                }
+                this._minder.getRenderContainer().setTranslate(position.round());
+                this._minder.fire("viewchange");
+            },
+            getMovement: function() {
+                var translate = this._minder.getRenderContainer().transform.translate;
+                return translate ? translate[0] : new kity.Point();
+            },
+            getView: function() {
+                var minder = this._minder;
+                var c = minder._lastClientSize || {
+                    width: minder.getRenderTarget().clientWidth,
+                    height: minder.getRenderTarget().clientHeight
+                };
+                var m = this.getMovement();
+                var box = new kity.Box(0, 0, c.width, c.height);
+                var viewMatrix = minder.getPaper().getViewPortMatrix();
+                return viewMatrix.inverse().translate(-m.x, -m.y).transformBox(box);
+            },
+            _bind: function() {
+                var dragger = this, isTempDrag = false, lastPosition = null, currentPosition = null;
+                function dragEnd(e) {
+                    if (!lastPosition) return;
+                    lastPosition = null;
+                    e.stopPropagation();
+                    // 临时拖动需要还原状态
+                    if (isTempDrag) {
+                        dragger.setEnabled(false);
+                        isTempDrag = false;
+                        if (dragger._minder.getStatus() == "hand") dragger._minder.rollbackStatus();
+                    }
+                    var paper = dragger._minder.getPaper();
+                    paper.setStyle("cursor", dragger._minder.getStatus() == "hand" ? "-webkit-grab" : "default");
+                    dragger._minder.fire("viewchanged");
+                }
+                this._minder.on("normal.mousedown normal.touchstart " + "inputready.mousedown inputready.touchstart " + "readonly.mousedown readonly.touchstart", function(e) {
+                    if (e.originEvent.button == 2) {
+                        e.originEvent.preventDefault();
+                    }
+                    // 点击未选中的根节点临时开启
+                    if (e.getTargetNode() == this.getRoot() || e.originEvent.button == 2 || e.originEvent.altKey) {
+                        lastPosition = e.getPosition("view");
+                        isTempDrag = true;
+                    }
+                }).on("normal.mousemove normal.touchmove " + "readonly.mousemove readonly.touchmove " + "inputready.mousemove inputready.touchmove", function(e) {
+                    if (e.type == "touchmove") {
+                        e.preventDefault();
+                    }
+                    if (!isTempDrag) return;
+                    var offset = kity.Vector.fromPoints(lastPosition, e.getPosition("view"));
+                    if (offset.length() > 10) {
+                        this.setStatus("hand", true);
+                        var paper = dragger._minder.getPaper();
+                        paper.setStyle("cursor", "-webkit-grabbing");
+                    }
+                }).on("hand.beforemousedown hand.beforetouchstart", function(e) {
+                    // 已经被用户打开拖放模式
+                    if (dragger.isEnabled()) {
+                        lastPosition = e.getPosition("view");
+                        e.stopPropagation();
+                        var paper = dragger._minder.getPaper();
+                        paper.setStyle("cursor", "-webkit-grabbing");
+                    }
+                }).on("hand.beforemousemove hand.beforetouchmove", function(e) {
+                    if (lastPosition) {
+                        currentPosition = e.getPosition("view");
+                        // 当前偏移加上历史偏移
+                        var offset = kity.Vector.fromPoints(lastPosition, currentPosition);
+                        dragger.move(offset);
+                        e.stopPropagation();
+                        e.preventDefault();
+                        e.originEvent.preventDefault();
+                        lastPosition = currentPosition;
+                    }
+                }).on("mouseup touchend", dragEnd);
+                window.addEventListener("mouseup", dragEnd);
+                this._minder.on("contextmenu", function(e) {
+                    e.preventDefault();
+                });
+            }
+        });
+        Module.register("View", function() {
+            var km = this;
+            /**
+         * @command Hand
+         * @description 切换抓手状态,抓手状态下,鼠标拖动将拖动视野,而不是创建选区
+         * @state
+         *   0: 当前不是抓手状态
+         *   1: 当前是抓手状态
+         */
+            var ToggleHandCommand = kity.createClass("ToggleHandCommand", {
+                base: Command,
+                execute: function(minder) {
+                    if (minder.getStatus() != "hand") {
+                        minder.setStatus("hand", true);
+                    } else {
+                        minder.rollbackStatus();
+                    }
+                    this.setContentChanged(false);
+                },
+                queryState: function(minder) {
+                    return minder.getStatus() == "hand" ? 1 : 0;
+                },
+                enableReadOnly: true
+            });
+            /**
+         * @command Camera
+         * @description 设置当前视野的中心位置到某个节点上
+         * @param {kityminder.MinderNode} focusNode 要定位的节点
+         * @param {number} duration 设置视野移动的动画时长(单位 ms),设置为 0 不使用动画
+         * @state
+         *   0: 始终可用
+         */
+            var CameraCommand = kity.createClass("CameraCommand", {
+                base: Command,
+                execute: function(km, focusNode) {
+                    focusNode = focusNode || km.getRoot();
+                    var viewport = km.getPaper().getViewPort();
+                    var offset = focusNode.getRenderContainer().getRenderBox("view");
+                    var dx = viewport.center.x - offset.x - offset.width / 2, dy = viewport.center.y - offset.y;
+                    var dragger = km._viewDragger;
+                    var duration = km.getOption("viewAnimationDuration");
+                    dragger.move(new kity.Point(dx, dy), duration);
+                    this.setContentChanged(false);
+                },
+                enableReadOnly: true
+            });
+            /**
+         * @command Move
+         * @description 指定方向移动当前视野
+         * @param {string} dir 移动方向
+         *    取值为 'left',视野向左移动一半
+         *    取值为 'right',视野向右移动一半
+         *    取值为 'up',视野向上移动一半
+         *    取值为 'down',视野向下移动一半
+         * @param {number} duration 视野移动的动画时长(单位 ms),设置为 0 不使用动画
+         * @state
+         *   0: 始终可用
+         */
+            var MoveCommand = kity.createClass("MoveCommand", {
+                base: Command,
+                execute: function(km, dir) {
+                    var dragger = km._viewDragger;
+                    var size = km._lastClientSize;
+                    var duration = km.getOption("viewAnimationDuration");
+                    switch (dir) {
+                      case "up":
+                        dragger.move(new kity.Point(0, size.height / 2), duration);
+                        break;
+
+                      case "down":
+                        dragger.move(new kity.Point(0, -size.height / 2), duration);
+                        break;
+
+                      case "left":
+                        dragger.move(new kity.Point(size.width / 2, 0), duration);
+                        break;
+
+                      case "right":
+                        dragger.move(new kity.Point(-size.width / 2, 0), duration);
+                        break;
+                    }
+                },
+                enableReadOnly: true
+            });
+            return {
+                init: function() {
+                    this._viewDragger = new ViewDragger(this);
+                },
+                commands: {
+                    hand: ToggleHandCommand,
+                    camera: CameraCommand,
+                    move: MoveCommand
+                },
+                events: {
+                    statuschange: function(e) {
+                        this._viewDragger.setEnabled(e.currentStatus == "hand");
+                    },
+                    mousewheel: function(e) {
+                        var dx, dy;
+                        e = e.originEvent;
+                        if (e.ctrlKey || e.shiftKey) return;
+                        if ("wheelDeltaX" in e) {
+                            dx = e.wheelDeltaX || 0;
+                            dy = e.wheelDeltaY || 0;
+                        } else {
+                            dx = 0;
+                            dy = e.wheelDelta;
+                        }
+                        this._viewDragger.move({
+                            x: dx / 2.5,
+                            y: dy / 2.5
+                        });
+                        var me = this;
+                        clearTimeout(this._mousewheeltimer);
+                        this._mousewheeltimer = setTimeout(function() {
+                            me.fire("viewchanged");
+                        }, 100);
+                        e.preventDefault();
+                    },
+                    "normal.dblclick readonly.dblclick": function(e) {
+                        if (e.kityEvent.targetShape instanceof kity.Paper) {
+                            this.execCommand("camera", this.getRoot(), 800);
+                        }
+                    },
+                    "paperrender finishInitHook": function() {
+                        if (!this.getRenderTarget()) {
+                            return;
+                        }
+                        this.execCommand("camera", null, 0);
+                        this._lastClientSize = {
+                            width: this.getRenderTarget().clientWidth,
+                            height: this.getRenderTarget().clientHeight
+                        };
+                    },
+                    resize: function(e) {
+                        var a = {
+                            width: this.getRenderTarget().clientWidth,
+                            height: this.getRenderTarget().clientHeight
+                        }, b = this._lastClientSize;
+                        this._viewDragger.move(new kity.Point((a.width - b.width) / 2 | 0, (a.height - b.height) / 2 | 0));
+                        this._lastClientSize = a;
+                    },
+                    "selectionchange layoutallfinish": function(e) {
+                        var selected = this.getSelectedNode();
+                        var minder = this;
+                        /*
+                    * Added by zhangbobell 2015.9.9
+                    * windows 10 的 edge 浏览器在全部动画停止后,优先级图标不显示 text,
+                    * 因此再次触发一次 render 事件,让浏览器重绘
+                    * */
+                        if (kity.Browser.edge) {
+                            this.fire("paperrender");
+                        }
+                        if (!selected) return;
+                        var dragger = this._viewDragger;
+                        var timeline = dragger.timeline();
+                        /*
+                    * Added by zhangbobell 2015.09.25
+                    * 如果之前有动画,那么就先暂时返回,等之前动画结束之后再次执行本函数
+                    * 以防止 view 动画变动了位置,导致本函数执行的时候位置计算不对
+                    *
+                    * fixed bug : 初始化的时候中心节点位置不固定(有的时候在左上角,有的时候在中心)
+                    * */
+                        if (timeline) {
+                            timeline.on("finish", function() {
+                                minder.fire("selectionchange");
+                            });
+                            return;
+                        }
+                        var view = dragger.getView();
+                        var focus = selected.getLayoutBox();
+                        var space = 50;
+                        var dx = 0, dy = 0;
+                        if (focus.right > view.right) {
+                            dx += view.right - focus.right - space;
+                        } else if (focus.left < view.left) {
+                            dx += view.left - focus.left + space;
+                        }
+                        if (focus.bottom > view.bottom) {
+                            dy += view.bottom - focus.bottom - space;
+                        }
+                        if (focus.top < view.top) {
+                            dy += view.top - focus.top + space;
+                        }
+                        if (dx || dy) dragger.move(new kity.Point(dx, dy), 100);
+                    }
+                }
+            };
+        });
+    }
+};
+
+//src/module/zoom.js
+_p[63] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var utils = _p.r(33);
+        var Minder = _p.r(19);
+        var MinderNode = _p.r(21);
+        var Command = _p.r(9);
+        var Module = _p.r(20);
+        var Renderer = _p.r(27);
+        Module.register("Zoom", function() {
+            var me = this;
+            var timeline;
+            function setTextRendering() {
+                var value = me._zoomValue >= 100 ? "optimize-speed" : "geometricPrecision";
+                me.getRenderContainer().setAttr("text-rendering", value);
+            }
+            function fixPaperCTM(paper) {
+                var node = paper.shapeNode;
+                var ctm = node.getCTM();
+                var matrix = new kity.Matrix(ctm.a, ctm.b, ctm.c, ctm.d, (ctm.e | 0) + .5, (ctm.f | 0) + .5);
+                node.setAttribute("transform", "matrix(" + matrix.toString() + ")");
+            }
+            kity.extendClass(Minder, {
+                zoom: function(value) {
+                    var paper = this.getPaper();
+                    var viewport = paper.getViewPort();
+                    viewport.zoom = value / 100;
+                    viewport.center = {
+                        x: viewport.center.x,
+                        y: viewport.center.y
+                    };
+                    paper.setViewPort(viewport);
+                    if (value == 100) fixPaperCTM(paper);
+                },
+                getZoomValue: function() {
+                    return this._zoomValue;
+                }
+            });
+            function zoomMinder(minder, value) {
+                var paper = minder.getPaper();
+                var viewport = paper.getViewPort();
+                if (!value) return;
+                setTextRendering();
+                var duration = minder.getOption("zoomAnimationDuration");
+                if (minder.getRoot().getComplex() > 200 || !duration) {
+                    minder._zoomValue = value;
+                    minder.zoom(value);
+                    minder.fire("viewchange");
+                } else {
+                    var animator = new kity.Animator({
+                        beginValue: minder._zoomValue,
+                        finishValue: value,
+                        setter: function(target, value) {
+                            target.zoom(value);
+                        }
+                    });
+                    minder._zoomValue = value;
+                    if (timeline) {
+                        timeline.pause();
+                    }
+                    timeline = animator.start(minder, duration, "easeInOutSine");
+                    timeline.on("finish", function() {
+                        minder.fire("viewchange");
+                    });
+                }
+                minder.fire("zoom", {
+                    zoom: value
+                });
+            }
+            /**
+         * @command Zoom
+         * @description 缩放当前的视野到一定的比例(百分比)
+         * @param {number} value 设置的比例,取值 100 则为原尺寸
+         * @state
+         *   0: 始终可用
+         */
+            var ZoomCommand = kity.createClass("Zoom", {
+                base: Command,
+                execute: zoomMinder,
+                queryValue: function(minder) {
+                    return minder._zoomValue;
+                }
+            });
+            /**
+         * @command ZoomIn
+         * @description 放大当前的视野到下一个比例等级(百分比)
+         * @shortcut =
+         * @state
+         *   0: 如果当前脑图的配置中还有下一个比例等级
+         *  -1: 其它情况
+         */
+            var ZoomInCommand = kity.createClass("ZoomInCommand", {
+                base: Command,
+                execute: function(minder) {
+                    zoomMinder(minder, this.nextValue(minder));
+                },
+                queryState: function(minder) {
+                    return +!this.nextValue(minder);
+                },
+                nextValue: function(minder) {
+                    var stack = minder.getOption("zoom"), i;
+                    for (i = 0; i < stack.length; i++) {
+                        if (stack[i] > minder._zoomValue) return stack[i];
+                    }
+                    return 0;
+                },
+                enableReadOnly: true
+            });
+            /**
+         * @command ZoomOut
+         * @description 缩小当前的视野到上一个比例等级(百分比)
+         * @shortcut -
+         * @state
+         *   0: 如果当前脑图的配置中还有上一个比例等级
+         *  -1: 其它情况
+         */
+            var ZoomOutCommand = kity.createClass("ZoomOutCommand", {
+                base: Command,
+                execute: function(minder) {
+                    zoomMinder(minder, this.nextValue(minder));
+                },
+                queryState: function(minder) {
+                    return +!this.nextValue(minder);
+                },
+                nextValue: function(minder) {
+                    var stack = minder.getOption("zoom"), i;
+                    for (i = stack.length - 1; i >= 0; i--) {
+                        if (stack[i] < minder._zoomValue) return stack[i];
+                    }
+                    return 0;
+                },
+                enableReadOnly: true
+            });
+            return {
+                init: function() {
+                    this._zoomValue = 100;
+                    this.setDefaultOptions({
+                        zoom: [ 10, 20, 50, 100, 200 ]
+                    });
+                    setTextRendering();
+                },
+                commands: {
+                    zoomin: ZoomInCommand,
+                    zoomout: ZoomOutCommand,
+                    zoom: ZoomCommand
+                },
+                events: {
+                    "normal.mousewheel readonly.mousewheel": function(e) {
+                        if (!e.originEvent.ctrlKey && !e.originEvent.metaKey) return;
+                        var delta = e.originEvent.wheelDelta;
+                        var me = this;
+                        // 稀释
+                        if (Math.abs(delta) > 100) {
+                            clearTimeout(this._wheelZoomTimeout);
+                        } else {
+                            return;
+                        }
+                        this._wheelZoomTimeout = setTimeout(function() {
+                            var value;
+                            var lastValue = me.getPaper()._zoom || 1;
+                            if (delta > 0) {
+                                me.execCommand("zoomin");
+                            } else if (delta < 0) {
+                                me.execCommand("zoomout");
+                            }
+                        }, 100);
+                        e.originEvent.preventDefault();
+                    }
+                },
+                commandShortcutKeys: {
+                    zoomin: "ctrl+=",
+                    zoomout: "ctrl+-"
+                }
+            };
+        });
+    }
+};
+
+//src/protocol/json.js
+_p[64] = {
+    value: function(require, exports, module) {
+        var data = _p.r(12);
+        data.registerProtocol("json", module.exports = {
+            fileDescription: "KityMinder 格式",
+            fileExtension: ".km",
+            dataType: "text",
+            mineType: "application/json",
+            encode: function(json) {
+                return JSON.stringify(json);
+            },
+            decode: function(local) {
+                return JSON.parse(local);
+            }
+        });
+    }
+};
+
+//src/protocol/markdown.js
+_p[65] = {
+    value: function(require, exports, module) {
+        var data = _p.r(12);
+        var LINE_ENDING_SPLITER = /\r\n|\r|\n/;
+        var EMPTY_LINE = "";
+        var NOTE_MARK_START = "\x3c!--Note--\x3e";
+        var NOTE_MARK_CLOSE = "\x3c!--/Note--\x3e";
+        function encode(json) {
+            return _build(json, 1).join("\n");
+        }
+        function _build(node, level) {
+            var lines = [];
+            level = level || 1;
+            var sharps = _generateHeaderSharp(level);
+            lines.push(sharps + " " + node.data.text);
+            lines.push(EMPTY_LINE);
+            var note = node.data.note;
+            if (note) {
+                var hasSharp = /^#/.test(note);
+                if (hasSharp) {
+                    lines.push(NOTE_MARK_START);
+                    note = note.replace(/^#+/gm, function($0) {
+                        return sharps + $0;
+                    });
+                }
+                lines.push(note);
+                if (hasSharp) {
+                    lines.push(NOTE_MARK_CLOSE);
+                }
+                lines.push(EMPTY_LINE);
+            }
+            if (node.children) node.children.forEach(function(child) {
+                lines = lines.concat(_build(child, level + 1));
+            });
+            return lines;
+        }
+        function _generateHeaderSharp(level) {
+            var sharps = "";
+            while (level--) sharps += "#";
+            return sharps;
+        }
+        function decode(markdown) {
+            var json, parentMap = {}, lines, line, lineInfo, level, node, parent, noteProgress, codeBlock;
+            // 一级标题转换 `{title}\n===` => `# {title}`
+            markdown = markdown.replace(/^(.+)\n={3,}/, function($0, $1) {
+                return "# " + $1;
+            });
+            lines = markdown.split(LINE_ENDING_SPLITER);
+            // 按行分析
+            for (var i = 0; i < lines.length; i++) {
+                line = lines[i];
+                lineInfo = _resolveLine(line);
+                // 备注标记处理
+                if (lineInfo.noteClose) {
+                    noteProgress = false;
+                    continue;
+                } else if (lineInfo.noteStart) {
+                    noteProgress = true;
+                    continue;
+                }
+                // 代码块处理
+                codeBlock = lineInfo.codeBlock ? !codeBlock : codeBlock;
+                // 备注条件:备注标签中,非标题定义,或标题越位
+                if (noteProgress || codeBlock || !lineInfo.level || lineInfo.level > level + 1) {
+                    if (node) _pushNote(node, line);
+                    continue;
+                }
+                // 标题处理
+                level = lineInfo.level;
+                node = _initNode(lineInfo.content, parentMap[level - 1]);
+                parentMap[level] = node;
+            }
+            _cleanUp(parentMap[1]);
+            return parentMap[1];
+        }
+        function _initNode(text, parent) {
+            var node = {
+                data: {
+                    text: text,
+                    note: ""
+                }
+            };
+            if (parent) {
+                if (parent.children) parent.children.push(node); else parent.children = [ node ];
+            }
+            return node;
+        }
+        function _pushNote(node, line) {
+            node.data.note += line + "\n";
+        }
+        function _isEmpty(line) {
+            return !/\S/.test(line);
+        }
+        function _resolveLine(line) {
+            var match = /^(#+)?\s*(.*)$/.exec(line);
+            return {
+                level: match[1] && match[1].length || null,
+                content: match[2],
+                noteStart: line == NOTE_MARK_START,
+                noteClose: line == NOTE_MARK_CLOSE,
+                codeBlock: /^\s*```/.test(line)
+            };
+        }
+        function _cleanUp(node) {
+            if (!/\S/.test(node.data.note)) {
+                node.data.note = null;
+                delete node.data.note;
+            } else {
+                var notes = node.data.note.split("\n");
+                while (notes.length && !/\S/.test(notes[0])) notes.shift();
+                while (notes.length && !/\S/.test(notes[notes.length - 1])) notes.pop();
+                node.data.note = notes.join("\n");
+            }
+            if (node.children) node.children.forEach(_cleanUp);
+        }
+        data.registerProtocol("markdown", module.exports = {
+            fileDescription: "Markdown/GFM 格式",
+            fileExtension: ".md",
+            mineType: "text/markdown",
+            dataType: "text",
+            encode: function(json) {
+                return encode(json.root);
+            },
+            decode: function(markdown) {
+                return decode(markdown);
+            }
+        });
+    }
+};
+
+//src/protocol/png.js
+_p[66] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var data = _p.r(12);
+        var Promise = _p.r(25);
+        var DomURL = window.URL || window.webkitURL || window;
+        function loadImage(info, callback) {
+            return new Promise(function(resolve, reject) {
+                var image = document.createElement("img");
+                image.onload = function() {
+                    resolve({
+                        element: this,
+                        x: info.x,
+                        y: info.y,
+                        width: info.width,
+                        height: info.height
+                    });
+                };
+                image.onerror = function(err) {
+                    reject(err);
+                };
+                image.crossOrigin = "anonymous";
+                image.src = info.url;
+            });
+        }
+        /**
+     * xhrLoadImage: 通过 xhr 加载保存在 BOS 上的图片
+     * @note: BOS 上的 CORS 策略是取 headers 里面的 Origin 字段进行判断
+     *        而通过 image 的 src 的方式是无法传递 origin 的,因此需要通过 xhr 进行
+     */
+        function xhrLoadImage(info, callback) {
+            return Promise(function(resolve, reject) {
+                var xmlHttp = new XMLHttpRequest();
+                xmlHttp.open("GET", info.url + "?_=" + Date.now(), true);
+                xmlHttp.responseType = "blob";
+                xmlHttp.onreadystatechange = function() {
+                    if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
+                        var blob = xmlHttp.response;
+                        var image = document.createElement("img");
+                        image.src = DomURL.createObjectURL(blob);
+                        image.onload = function() {
+                            DomURL.revokeObjectURL(image.src);
+                            resolve({
+                                element: image,
+                                x: info.x,
+                                y: info.y,
+                                width: info.width,
+                                height: info.height
+                            });
+                        };
+                    }
+                };
+                xmlHttp.send();
+            });
+        }
+        function getSVGInfo(minder) {
+            var paper = minder.getPaper(), paperTransform, domContainer = paper.container, svgXml, svgContainer, svgDom, renderContainer = minder.getRenderContainer(), renderBox = renderContainer.getRenderBox(), width = renderBox.width + 1, height = renderBox.height + 1, blob, svgUrl, img;
+            // 保存原始变换,并且移动到合适的位置
+            paperTransform = paper.shapeNode.getAttribute("transform");
+            paper.shapeNode.setAttribute("transform", "translate(0.5, 0.5)");
+            renderContainer.translate(-renderBox.x, -renderBox.y);
+            // 获取当前的 XML 代码
+            svgXml = paper.container.innerHTML;
+            // 回复原始变换及位置
+            renderContainer.translate(renderBox.x, renderBox.y);
+            paper.shapeNode.setAttribute("transform", paperTransform);
+            // 过滤内容
+            svgContainer = document.createElement("div");
+            svgContainer.innerHTML = svgXml;
+            svgDom = svgContainer.querySelector("svg");
+            svgDom.setAttribute("width", renderBox.width + 1);
+            svgDom.setAttribute("height", renderBox.height + 1);
+            svgDom.setAttribute("style", 'font-family: Arial, "Microsoft Yahei","Heiti SC";');
+            svgContainer = document.createElement("div");
+            svgContainer.appendChild(svgDom);
+            svgXml = svgContainer.innerHTML;
+            // Dummy IE
+            svgXml = svgXml.replace(' xmlns="http://www.w3.org/2000/svg" ' + 'xmlns:NS1="" NS1:ns1:xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:NS2="" NS2:xmlns:ns1=""', "");
+            // svg 含有 &nbsp; 符号导出报错 Entity 'nbsp' not defined ,含有控制字符触发Load Image 会触发报错
+            svgXml = svgXml.replace(/&nbsp;|[\x00-\x1F\x7F-\x9F]/g, "");
+            // fix title issue in safari
+            // @ http://stackoverflow.com/questions/30273775/namespace-prefix-ns1-for-href-on-tagelement-is-not-defined-setattributens
+            svgXml = svgXml.replace(/NS\d+:title/gi, "xlink:title");
+            blob = new Blob([ svgXml ], {
+                type: "image/svg+xml"
+            });
+            svgUrl = DomURL.createObjectURL(blob);
+            //svgUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgXml);
+            var imagesInfo = [];
+            // 遍历取出图片信息
+            traverse(minder.getRoot());
+            function traverse(node) {
+                var nodeData = node.data;
+                if (nodeData.image) {
+                    minder.renderNode(node);
+                    var nodeData = node.data;
+                    var imageUrl = nodeData.image;
+                    var imageSize = nodeData.imageSize;
+                    var imageRenderBox = node.getRenderBox("ImageRenderer", minder.getRenderContainer());
+                    var imageInfo = {
+                        url: imageUrl,
+                        width: imageSize.width,
+                        height: imageSize.height,
+                        x: -renderContainer.getBoundaryBox().x + imageRenderBox.x,
+                        y: -renderContainer.getBoundaryBox().y + imageRenderBox.y
+                    };
+                    imagesInfo.push(imageInfo);
+                }
+                // 若节点折叠,则直接返回
+                if (nodeData.expandState === "collapse") {
+                    return;
+                }
+                var children = node.getChildren();
+                for (var i = 0; i < children.length; i++) {
+                    traverse(children[i]);
+                }
+            }
+            return {
+                width: width,
+                height: height,
+                dataUrl: svgUrl,
+                xml: svgXml,
+                imagesInfo: imagesInfo
+            };
+        }
+        function encode(json, minder, option) {
+            var resultCallback;
+            /* 绘制 PNG 的画布及上下文 */
+            var canvas = document.createElement("canvas");
+            var ctx = canvas.getContext("2d");
+            /* 尝试获取背景图片 URL 或背景颜色 */
+            var bgDeclare = minder.getStyle("background").toString();
+            var bgUrl = /url\(\"(.+)\"\)/.exec(bgDeclare);
+            var bgColor = kity.Color.parse(bgDeclare);
+            /* 获取 SVG 文件内容 */
+            var svgInfo = getSVGInfo(minder);
+            var width = option && option.width && option.width > svgInfo.width ? option.width : svgInfo.width;
+            var height = option && option.height && option.height > svgInfo.height ? option.height : svgInfo.height;
+            var offsetX = option && option.width && option.width > svgInfo.width ? (option.width - svgInfo.width) / 2 : 0;
+            var offsetY = option && option.height && option.height > svgInfo.height ? (option.height - svgInfo.height) / 2 : 0;
+            var svgDataUrl = svgInfo.dataUrl;
+            var imagesInfo = svgInfo.imagesInfo;
+            /* 画布的填充大小 */
+            var padding = 20;
+            canvas.width = width + padding * 2;
+            canvas.height = height + padding * 2;
+            function fillBackground(ctx, style) {
+                ctx.save();
+                ctx.fillStyle = style;
+                ctx.fillRect(0, 0, canvas.width, canvas.height);
+                ctx.restore();
+            }
+            function drawImage(ctx, image, x, y, width, height) {
+                if (width && height) {
+                    ctx.drawImage(image, x + padding, y + padding, width, height);
+                } else {
+                    ctx.drawImage(image, x + padding, y + padding);
+                }
+            }
+            function generateDataUrl(canvas) {
+                return canvas.toDataURL("image/png");
+            }
+            // 加载节点上的图片
+            function loadImages(imagesInfo) {
+                var imagePromises = imagesInfo.map(function(imageInfo) {
+                    return xhrLoadImage(imageInfo);
+                });
+                return Promise.all(imagePromises);
+            }
+            function drawSVG() {
+                var svgData = {
+                    url: svgDataUrl
+                };
+                return loadImage(svgData).then(function($image) {
+                    drawImage(ctx, $image.element, offsetX, offsetY, $image.width, $image.height);
+                    return loadImages(imagesInfo);
+                }).then(function($images) {
+                    for (var i = 0; i < $images.length; i++) {
+                        drawImage(ctx, $images[i].element, $images[i].x + offsetX, $images[i].y + offsetY, $images[i].width, $images[i].height);
+                    }
+                    DomURL.revokeObjectURL(svgDataUrl);
+                    document.body.appendChild(canvas);
+                    var pngBase64 = generateDataUrl(canvas);
+                    document.body.removeChild(canvas);
+                    return pngBase64;
+                }, function(err) {
+                    // 这里处理 reject,出错基本上是因为跨域,
+                    // 出错后依然导出,只不过没有图片。
+                    alert("脑图的节点中包含跨域图片,导出的 png 中节点图片不显示,你可以替换掉这些跨域的图片并重试。");
+                    DomURL.revokeObjectURL(svgDataUrl);
+                    document.body.appendChild(canvas);
+                    var pngBase64 = generateDataUrl(canvas);
+                    document.body.removeChild(canvas);
+                    return pngBase64;
+                });
+            }
+            if (bgUrl) {
+                var bgInfo = {
+                    url: bgUrl[1]
+                };
+                return loadImage(bgInfo).then(function($image) {
+                    fillBackground(ctx, ctx.createPattern($image.element, "repeat"));
+                    return drawSVG();
+                });
+            } else {
+                fillBackground(ctx, bgColor.toString());
+                return drawSVG();
+            }
+        }
+        data.registerProtocol("png", module.exports = {
+            fileDescription: "PNG 图片",
+            fileExtension: ".png",
+            mineType: "image/png",
+            dataType: "base64",
+            encode: encode
+        });
+    }
+};
+
+//src/protocol/svg.js
+_p[67] = {
+    value: function(require, exports, module) {
+        var data = _p.r(12);
+        /**
+     * 导出svg时删除全部svg元素中的transform
+     * @auth Naixor
+     * @method removeTransform
+     * @param  {[type]}        svgDom [description]
+     * @return {[type]}               [description]
+     */
+        function cleanSVG(svgDom, x, y) {
+            function getTransformToElement(target, source) {
+                var matrix;
+                try {
+                    matrix = source.getScreenCTM().inverse();
+                } catch (e) {
+                    throw new Error("Can not inverse source element' ctm.");
+                }
+                return matrix.multiply(target.getScreenCTM());
+            }
+            function dealWithPath(d, dealWithPattern) {
+                if (!(dealWithPattern instanceof Function)) {
+                    dealWithPattern = function() {};
+                }
+                var strArr = [], pattern = [], cache = [];
+                for (var i = 0, l = d.length; i < l; i++) {
+                    switch (d[i]) {
+                      case "M":
+                      case "L":
+                      case "T":
+                      case "S":
+                      case "A":
+                      case "C":
+                      case "H":
+                      case "V":
+                      case "Q":
+                        {
+                            if (cache.length) {
+                                pattern.push(cache.join(""));
+                                cache = [];
+                            }
+                            // 脑图的path格式真奇怪...偶尔就给我蹦出来一个"..V123 C..", 那空格几个意思 - -
+                            if (pattern[pattern.length - 1] === ",") {
+                                pattern.pop();
+                            }
+                            if (pattern.length) {
+                                dealWithPattern(pattern);
+                                strArr.push(pattern.join(""));
+                                pattern = [];
+                            }
+                            pattern.push(d[i]);
+                            break;
+                        }
+
+                      case "Z":
+                      case "z":
+                        {
+                            pattern.push(cache.join(""), d[i]);
+                            dealWithPattern(pattern);
+                            strArr.push(pattern.join(""));
+                            cache = [];
+                            pattern = [];
+                            break;
+                        }
+
+                      case ".":
+                      case "e":
+                        {
+                            cache.push(d[i]);
+                            break;
+                        }
+
+                      case "-":
+                        {
+                            if (d[i - 1] !== "e") {
+                                if (cache.length) {
+                                    pattern.push(cache.join(""), ",");
+                                }
+                                cache = [];
+                            }
+                            cache.push("-");
+                            break;
+                        }
+
+                      case " ":
+                      case ",":
+                        {
+                            if (cache.length) {
+                                pattern.push(cache.join(""), ",");
+                                cache = [];
+                            }
+                            break;
+                        }
+
+                      default:
+                        {
+                            if (/\d/.test(d[i])) {
+                                cache.push(d[i]);
+                            } else {
+                                // m a c s q h v l t z情况
+                                if (cache.length) {
+                                    pattern.push(cache.join(""), d[i]);
+                                    cache = [];
+                                } else {
+                                    // 脑图的path格式真奇怪...偶尔就给我蹦出来一个"..V123 c..", 那空格几个意思 - -
+                                    if (pattern[pattern.length - 1] === ",") {
+                                        pattern.pop();
+                                    }
+                                    pattern.push(d[i]);
+                                }
+                            }
+                            if (i + 1 === l) {
+                                if (cache.length) {
+                                    pattern.push(cache.join(""));
+                                }
+                                dealWithPattern(pattern);
+                                strArr.push(pattern.join(""));
+                                cache = null;
+                                pattern = null;
+                            }
+                        }
+                    }
+                }
+                return strArr.join("");
+            }
+            function replaceWithNode(svgNode, parentX, parentY) {
+                if (!svgNode) {
+                    return;
+                }
+                if (svgNode.tagName === "defs") {
+                    return;
+                }
+                if (svgNode.getAttribute("fill") === "transparent") {
+                    svgNode.setAttribute("fill", "none");
+                }
+                if (svgNode.getAttribute("marker-end")) {
+                    svgNode.removeAttribute("marker-end");
+                }
+                parentX = parentX || 0;
+                parentY = parentY || 0;
+                if (svgNode.getAttribute("transform")) {
+                    var ctm = getTransformToElement(svgNode, svgNode.parentElement);
+                    parentX -= ctm.e;
+                    parentY -= ctm.f;
+                    svgNode.removeAttribute("transform");
+                }
+                switch (svgNode.tagName.toLowerCase()) {
+                  case "g":
+                    break;
+
+                  case "path":
+                    {
+                        var d = svgNode.getAttribute("d");
+                        if (d) {
+                            d = dealWithPath(d, function(pattern) {
+                                switch (pattern[0]) {
+                                  case "V":
+                                    {
+                                        pattern[1] = +pattern[1] - parentY;
+                                        break;
+                                    }
+
+                                  case "H":
+                                    {
+                                        pattern[1] = +pattern[1] - parentX;
+                                        break;
+                                    }
+
+                                  case "M":
+                                  case "L":
+                                  case "T":
+                                    {
+                                        pattern[1] = +pattern[1] - parentX;
+                                        pattern[3] = +pattern[3] - parentY;
+                                        break;
+                                    }
+
+                                  case "Q":
+                                  case "S":
+                                    {
+                                        pattern[1] = +pattern[1] - parentX;
+                                        pattern[3] = +pattern[3] - parentY;
+                                        pattern[5] = +pattern[5] - parentX;
+                                        pattern[7] = +pattern[7] - parentY;
+                                        break;
+                                    }
+
+                                  case "A":
+                                    {
+                                        pattern[11] = +pattern[11] - parentX;
+                                        pattern[13] = +pattern[13] - parentY;
+                                        break;
+                                    }
+
+                                  case "C":
+                                    {
+                                        pattern[1] = +pattern[1] - parentX;
+                                        pattern[3] = +pattern[3] - parentY;
+                                        pattern[5] = +pattern[5] - parentX;
+                                        pattern[7] = +pattern[7] - parentY;
+                                        pattern[9] = +pattern[9] - parentX;
+                                        pattern[11] = +pattern[11] - parentY;
+                                    }
+                                }
+                            });
+                            svgNode.setAttribute("d", d);
+                            svgNode.removeAttribute("transform");
+                        }
+                        return;
+                    }
+
+                  case "image":
+                  case "text":
+                    {
+                        if (parentX && parentY) {
+                            var x = +svgNode.getAttribute("x") || 0, y = +svgNode.getAttribute("y") || 0;
+                            svgNode.setAttribute("x", x - parentX);
+                            svgNode.setAttribute("y", y - parentY);
+                        }
+                        if (svgNode.getAttribute("dominant-baseline")) {
+                            svgNode.removeAttribute("dominant-baseline");
+                            svgNode.setAttribute("dy", ".8em");
+                        }
+                        svgNode.removeAttribute("transform");
+                        return;
+                    }
+                }
+                if (svgNode.children) {
+                    for (var i = 0, l = svgNode.children.length; i < l; i++) {
+                        replaceWithNode(svgNode.children[i], parentX, parentY);
+                    }
+                }
+            }
+            svgDom.style.visibility = "hidden";
+            replaceWithNode(svgDom, x || 0, y || 0);
+            svgDom.style.visibility = "visible";
+        }
+        data.registerProtocol("svg", module.exports = {
+            fileDescription: "SVG 矢量图",
+            fileExtension: ".svg",
+            mineType: "image/svg+xml",
+            dataType: "text",
+            encode: function(json, minder) {
+                var paper = minder.getPaper(), paperTransform = paper.shapeNode.getAttribute("transform"), svgXml, svgContainer, svgDom, renderContainer = minder.getRenderContainer(), renderBox = renderContainer.getRenderBox(), transform = renderContainer.getTransform(), width = renderBox.width, height = renderBox.height, padding = 20;
+                paper.shapeNode.setAttribute("transform", "translate(0.5, 0.5)");
+                svgXml = paper.container.innerHTML;
+                paper.shapeNode.setAttribute("transform", paperTransform);
+                svgContainer = document.createElement("div");
+                document.body.appendChild(svgContainer);
+                svgContainer.innerHTML = svgXml;
+                svgDom = svgContainer.querySelector("svg");
+                svgDom.setAttribute("width", width + padding * 2 | 0);
+                svgDom.setAttribute("height", height + padding * 2 | 0);
+                svgDom.setAttribute("style", "background: " + minder.getStyle("background"));
+                //"font-family: Arial, Microsoft Yahei, Heiti SC; " +
+                svgDom.setAttribute("viewBox", [ 0, 0, width + padding * 2 | 0, height + padding * 2 | 0 ].join(" "));
+                tempSvgContainer = document.createElement("div");
+                cleanSVG(svgDom, renderBox.x - padding | 0, renderBox.y - padding | 0);
+                document.body.removeChild(svgContainer);
+                tempSvgContainer.appendChild(svgDom);
+                // need a xml with width and height
+                svgXml = tempSvgContainer.innerHTML;
+                // svg 含有 &nbsp; 符号导出报错 Entity 'nbsp' not defined
+                svgXml = svgXml.replace(/&nbsp;/g, "&#xa0;");
+                // svg 含有 &nbsp; 符号导出报错 Entity 'nbsp' not defined
+                return svgXml;
+            }
+        });
+    }
+};
+
+//src/protocol/text.js
+_p[68] = {
+    value: function(require, exports, module) {
+        var data = _p.r(12);
+        var Browser = _p.r(17).Browser;
+        /**
+     * @Desc: 增加对不容浏览器下节点中文本\t匹配的处理,不同浏览器下\t无法正确匹配,导致无法使用TAB来批量导入节点
+     * @Editor: Naixor
+     * @Date: 2015.9.17
+     */
+        var LINE_ENDING = "\r", LINE_ENDING_SPLITER = /\r\n|\r|\n/, TAB_CHAR = function(Browser) {
+            if (Browser.gecko) {
+                return {
+                    REGEXP: new RegExp("^(\t|" + String.fromCharCode(160, 160, 32, 160) + ")"),
+                    DELETE: new RegExp("^(\t|" + String.fromCharCode(160, 160, 32, 160) + ")+")
+                };
+            } else if (Browser.ie || Browser.edge) {
+                // ie系列和edge比较特别,\t在div中会被直接转义成SPACE故只好使用SPACE来做处理
+                return {
+                    REGEXP: new RegExp("^(" + String.fromCharCode(32) + "|" + String.fromCharCode(160) + ")"),
+                    DELETE: new RegExp("^(" + String.fromCharCode(32) + "|" + String.fromCharCode(160) + ")+")
+                };
+            } else {
+                return {
+                    REGEXP: /^(\t|\x20{4})/,
+                    DELETE: /^(\t|\x20{4})+/
+                };
+            }
+        }(Browser);
+        function repeat(s, n) {
+            var result = "";
+            while (n--) result += s;
+            return result;
+        }
+        /**
+     * 对节点text中的换行符进行处理
+     * @method encodeWrap
+     * @param  {String}   nodeText MinderNode.data.text
+     * @return {String}            \n -> '\n'; \\n -> '\\n'
+     */
+        function encodeWrap(nodeText) {
+            if (!nodeText) {
+                return "";
+            }
+            var textArr = [], WRAP_TEXT = [ "\\", "n" ];
+            for (var i = 0, j = 0, l = nodeText.length; i < l; i++) {
+                if (nodeText[i] === "\n" || nodeText[i] === "\r") {
+                    textArr.push("\\n");
+                    j = 0;
+                    continue;
+                }
+                if (nodeText[i] === WRAP_TEXT[j]) {
+                    j++;
+                    if (j === 2) {
+                        j = 0;
+                        textArr.push("\\\\n");
+                    }
+                    continue;
+                }
+                switch (j) {
+                  case 0:
+                    {
+                        textArr.push(nodeText[i]);
+                        break;
+                    }
+
+                  case 1:
+                    {
+                        textArr.push(nodeText[i - 1], nodeText[i]);
+                    }
+                }
+                j = 0;
+            }
+            return textArr.join("");
+        }
+        /**
+     * 将文本内容中的'\n'和'\\n'分别转换成\n和\\n
+     * @method decodeWrap
+     * @param  {[type]}   text [description]
+     * @return {[type]}        [description]
+     */
+        function decodeWrap(text) {
+            if (!text) {
+                return "";
+            }
+            var textArr = [], WRAP_TEXT = [ "\\", "\\", "n" ];
+            for (var i = 0, j = 0, l = text.length; i < l; i++) {
+                if (text[i] === WRAP_TEXT[j]) {
+                    j++;
+                    if (j === 3) {
+                        j = 0;
+                        textArr.push("\\n");
+                    }
+                    continue;
+                }
+                switch (j) {
+                  case 0:
+                    {
+                        textArr.push(text[i]);
+                        j = 0;
+                        break;
+                    }
+
+                  case 1:
+                    {
+                        if (text[i] === "n") {
+                            textArr.push("\n");
+                        } else {
+                            textArr.push(text[i - 1], text[i]);
+                        }
+                        j = 0;
+                        break;
+                    }
+
+                  case 2:
+                    {
+                        textArr.push(text[i - 2]);
+                        if (text[i] !== "\\") {
+                            j = 0;
+                            textArr.push(text[i - 1], text[i]);
+                        }
+                        break;
+                    }
+                }
+            }
+            return textArr.join("");
+        }
+        function encode(json, level) {
+            var local = "";
+            level = level || 0;
+            local += repeat("\t", level);
+            local += encodeWrap(json.data.text) + LINE_ENDING;
+            if (json.children) {
+                json.children.forEach(function(child) {
+                    local += encode(child, level + 1);
+                });
+            }
+            return local;
+        }
+        function isEmpty(line) {
+            return !/\S/.test(line);
+        }
+        function getLevel(line) {
+            var level = 0;
+            while (TAB_CHAR.REGEXP.test(line)) {
+                line = line.replace(TAB_CHAR.REGEXP, "");
+                level++;
+            }
+            return level;
+        }
+        function getNode(line) {
+            return {
+                data: {
+                    text: decodeWrap(line.replace(TAB_CHAR.DELETE, ""))
+                }
+            };
+        }
+        function decode(local) {
+            var json, parentMap = {}, lines = local.split(LINE_ENDING_SPLITER), line, level, node;
+            function addChild(parent, child) {
+                var children = parent.children || (parent.children = []);
+                children.push(child);
+            }
+            for (var i = 0; i < lines.length; i++) {
+                line = lines[i];
+                if (isEmpty(line)) continue;
+                level = getLevel(line);
+                node = getNode(line);
+                if (level === 0) {
+                    if (json) {
+                        throw new Error("Invalid local format");
+                    }
+                    json = node;
+                } else {
+                    if (!parentMap[level - 1]) {
+                        throw new Error("Invalid local format");
+                    }
+                    addChild(parentMap[level - 1], node);
+                }
+                parentMap[level] = node;
+            }
+            return json;
+        }
+        /**
+     * @Desc: 增加一个将当前选中节点转换成text的方法
+     * @Editor: Naixor
+     * @Date: 2015.9.21
+     */
+        function Node2Text(node) {
+            function exportNode(node) {
+                var exported = {};
+                exported.data = node.getData();
+                var childNodes = node.getChildren();
+                exported.children = [];
+                for (var i = 0; i < childNodes.length; i++) {
+                    exported.children.push(exportNode(childNodes[i]));
+                }
+                return exported;
+            }
+            if (!node) return;
+            if (/^\s*$/.test(node.data.text)) {
+                node.data.text = "分支主题";
+            }
+            return encode(exportNode(node));
+        }
+        data.registerProtocol("text", module.exports = {
+            fileDescription: "大纲文本",
+            fileExtension: ".txt",
+            dataType: "text",
+            mineType: "text/plain",
+            encode: function(json) {
+                return encode(json.root, 0);
+            },
+            decode: function(local) {
+                return decode(local);
+            },
+            Node2Text: function(node) {
+                return Node2Text(node);
+            }
+        });
+    }
+};
+
+//src/template/default.js
+/**
+ * @fileOverview
+ *
+ * 默认模板 - 脑图模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[69] = {
+    value: function(require, exports, module) {
+        var template = _p.r(31);
+        template.register("default", {
+            getLayout: function(node) {
+                if (node.getData("layout")) return node.getData("layout");
+                var level = node.getLevel();
+                // 根节点
+                if (level === 0) {
+                    return "mind";
+                }
+                // 一级节点
+                if (level === 1) {
+                    return node.getLayoutPointPreview().x > 0 ? "right" : "left";
+                }
+                return node.parent.getLayout();
+            },
+            getConnect: function(node) {
+                if (node.getLevel() == 1) return "arc";
+                return "under";
+            }
+        });
+    }
+};
+
+//src/template/filetree.js
+/**
+ * @fileOverview
+ *
+ * 文件夹模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[70] = {
+    value: function(require, exports, module) {
+        var template = _p.r(31);
+        template.register("filetree", {
+            getLayout: function(node) {
+                if (node.getData("layout")) return node.getData("layout");
+                if (node.isRoot()) return "bottom";
+                return "filetree-down";
+            },
+            getConnect: function(node) {
+                if (node.getLevel() == 1) {
+                    return "poly";
+                }
+                return "l";
+            }
+        });
+    }
+};
+
+//src/template/fish-bone.js
+/**
+ * @fileOverview
+ *
+ * 默认模板 - 鱼骨头模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[71] = {
+    value: function(require, exports, module) {
+        var template = _p.r(31);
+        template.register("fish-bone", {
+            getLayout: function(node) {
+                if (node.getData("layout")) return node.getData("layout");
+                var level = node.getLevel();
+                // 根节点
+                if (level === 0) {
+                    return "fish-bone-master";
+                }
+                // 一级节点
+                if (level === 1) {
+                    return "fish-bone-slave";
+                }
+                return node.getLayoutPointPreview().y > 0 ? "filetree-up" : "filetree-down";
+            },
+            getConnect: function(node) {
+                switch (node.getLevel()) {
+                  case 1:
+                    return "fish-bone-master";
+
+                  case 2:
+                    return "line";
+
+                  default:
+                    return "l";
+                }
+            }
+        });
+    }
+};
+
+//src/template/right.js
+/**
+ * @fileOverview
+ *
+ * 往右布局结构模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[72] = {
+    value: function(require, exports, module) {
+        var template = _p.r(31);
+        template.register("right", {
+            getLayout: function(node) {
+                return node.getData("layout") || "right";
+            },
+            getConnect: function(node) {
+                if (node.getLevel() == 1) return "arc";
+                return "bezier";
+            }
+        });
+    }
+};
+
+//src/template/structure.js
+/**
+ * @fileOverview
+ *
+ * 组织结构图模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+_p[73] = {
+    value: function(require, exports, module) {
+        var template = _p.r(31);
+        template.register("structure", {
+            getLayout: function(node) {
+                return node.getData("layout") || "bottom";
+            },
+            getConnect: function(node) {
+                return "poly";
+            }
+        });
+    }
+};
+
+//src/template/tianpan.js
+/**
+ * @fileOverview
+ *
+ * 天盘模板
+ *
+ * @author: along
+ * @copyright: bpd729@163.com, 2015
+ */
+_p[74] = {
+    value: function(require, exports, module) {
+        var template = _p.r(31);
+        template.register("tianpan", {
+            getLayout: function(node) {
+                if (node.getData("layout")) return node.getData("layout");
+                var level = node.getLevel();
+                // 根节点
+                if (level === 0) {
+                    return "tianpan";
+                }
+                return node.parent.getLayout();
+            },
+            getConnect: function(node) {
+                return "arc_tp";
+            }
+        });
+    }
+};
+
+//src/theme/default.js
+_p[75] = {
+    value: function(require, exports, module) {
+        var theme = _p.r(32);
+        [ "classic", "classic-compact" ].forEach(function(name) {
+            var compact = name == "classic-compact";
+            /* jscs:disable maximumLineLength */
+            theme.register(name, {
+                background: '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+                "root-color": "#430",
+                "root-background": "#e9df98",
+                "root-stroke": "#e9df98",
+                "root-font-size": 24,
+                "root-padding": compact ? [ 10, 25 ] : [ 15, 25 ],
+                "root-margin": compact ? [ 15, 25 ] : [ 30, 100 ],
+                "root-radius": 30,
+                "root-space": 10,
+                "root-shadow": "rgba(0, 0, 0, .25)",
+                "main-color": "#333",
+                "main-background": "#a4c5c0",
+                "main-stroke": "#a4c5c0",
+                "main-font-size": 16,
+                "main-padding": compact ? [ 5, 15 ] : [ 6, 20 ],
+                "main-margin": compact ? [ 5, 10 ] : 20,
+                "main-radius": 10,
+                "main-space": 5,
+                "main-shadow": "rgba(0, 0, 0, .25)",
+                "sub-color": "white",
+                "sub-background": "transparent",
+                "sub-stroke": "none",
+                "sub-font-size": 12,
+                "sub-padding": [ 5, 10 ],
+                "sub-margin": compact ? [ 5, 10 ] : [ 15, 20 ],
+                "sub-tree-margin": 30,
+                "sub-radius": 5,
+                "sub-space": 5,
+                "connect-color": "white",
+                "connect-width": 2,
+                "main-connect-width": 3,
+                "connect-radius": 5,
+                "selected-background": "rgb(254, 219, 0)",
+                "selected-stroke": "rgb(254, 219, 0)",
+                "selected-color": "black",
+                "marquee-background": "rgba(255,255,255,.3)",
+                "marquee-stroke": "white",
+                "drop-hint-color": "yellow",
+                "sub-drop-hint-width": 2,
+                "main-drop-hint-width": 4,
+                "root-drop-hint-width": 4,
+                "order-hint-area-color": "rgba(0, 255, 0, .5)",
+                "order-hint-path-color": "#0f0",
+                "order-hint-path-width": 1,
+                "text-selection-color": "rgb(27,171,255)",
+                "line-height": 1.5
+            });
+        });
+    }
+};
+
+//src/theme/fish.js
+_p[76] = {
+    value: function(require, exports, module) {
+        var theme = _p.r(32);
+        theme.register("fish", {
+            background: '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+            "root-color": "#430",
+            "root-background": "#e9df98",
+            "root-stroke": "#e9df98",
+            "root-font-size": 24,
+            "root-padding": [ 35, 35 ],
+            "root-margin": 30,
+            "root-radius": 100,
+            "root-space": 10,
+            "root-shadow": "rgba(0, 0, 0, .25)",
+            "main-color": "#333",
+            "main-background": "#a4c5c0",
+            "main-stroke": "#a4c5c0",
+            "main-font-size": 16,
+            "main-padding": [ 6, 20 ],
+            "main-margin": [ 20, 20 ],
+            "main-radius": 5,
+            "main-space": 5,
+            "main-shadow": "rgba(0, 0, 0, .25)",
+            "sub-color": "black",
+            "sub-background": "white",
+            "sub-stroke": "white",
+            "sub-font-size": 12,
+            "sub-padding": [ 5, 10 ],
+            "sub-margin": [ 10 ],
+            "sub-radius": 5,
+            "sub-space": 5,
+            "connect-color": "white",
+            "connect-width": 3,
+            "main-connect-width": 3,
+            "connect-radius": 5,
+            "selected-background": "rgb(254, 219, 0)",
+            "selected-stroke": "rgb(254, 219, 0)",
+            "marquee-background": "rgba(255,255,255,.3)",
+            "marquee-stroke": "white",
+            "drop-hint-color": "yellow",
+            "drop-hint-width": 4,
+            "order-hint-area-color": "rgba(0, 255, 0, .5)",
+            "order-hint-path-color": "#0f0",
+            "order-hint-path-width": 1,
+            "text-selection-color": "rgb(27,171,255)",
+            "line-height": 1.5
+        });
+    }
+};
+
+//src/theme/fresh.js
+_p[77] = {
+    value: function(require, exports, module) {
+        var kity = _p.r(17);
+        var theme = _p.r(32);
+        function hsl(h, s, l) {
+            return kity.Color.createHSL(h, s, l);
+        }
+        function generate(h, compat) {
+            return {
+                background: "#fbfbfb",
+                "root-color": "white",
+                "root-background": hsl(h, 37, 60),
+                "root-stroke": hsl(h, 37, 60),
+                "root-font-size": 16,
+                "root-padding": compat ? [ 6, 12 ] : [ 12, 24 ],
+                "root-margin": compat ? 10 : [ 30, 100 ],
+                "root-radius": 5,
+                "root-space": 10,
+                "main-color": "black",
+                "main-background": hsl(h, 33, 95),
+                "main-stroke": hsl(h, 37, 60),
+                "main-stroke-width": 1,
+                "main-font-size": 14,
+                "main-padding": [ 6, 20 ],
+                "main-margin": compat ? 8 : 20,
+                "main-radius": 3,
+                "main-space": 5,
+                "sub-color": "black",
+                "sub-background": "transparent",
+                "sub-stroke": "none",
+                "sub-font-size": 12,
+                "sub-padding": compat ? [ 3, 5 ] : [ 5, 10 ],
+                "sub-margin": compat ? [ 4, 8 ] : [ 15, 20 ],
+                "sub-radius": 5,
+                "sub-space": 5,
+                "connect-color": hsl(h, 37, 60),
+                "connect-width": 1,
+                "connect-radius": 5,
+                "selected-stroke": hsl(h, 26, 30),
+                "selected-stroke-width": "3",
+                "blur-selected-stroke": hsl(h, 10, 60),
+                "marquee-background": hsl(h, 100, 80).set("a", .1),
+                "marquee-stroke": hsl(h, 37, 60),
+                "drop-hint-color": hsl(h, 26, 35),
+                "drop-hint-width": 5,
+                "order-hint-area-color": hsl(h, 100, 30).set("a", .5),
+                "order-hint-path-color": hsl(h, 100, 25),
+                "order-hint-path-width": 1,
+                "text-selection-color": hsl(h, 100, 20),
+                "line-height": 1.5
+            };
+        }
+        var plans = {
+            red: 0,
+            soil: 25,
+            green: 122,
+            blue: 204,
+            purple: 246,
+            pink: 334
+        };
+        var name;
+        for (name in plans) {
+            theme.register("fresh-" + name, generate(plans[name]));
+            theme.register("fresh-" + name + "-compat", generate(plans[name], true));
+        }
+    }
+};
+
+//src/theme/snow.js
+_p[78] = {
+    value: function(require, exports, module) {
+        var theme = _p.r(32);
+        [ "snow", "snow-compact" ].forEach(function(name) {
+            var compact = name == "snow-compact";
+            /* jscs:disable maximumLineLength */
+            theme.register(name, {
+                background: '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+                "root-color": "#430",
+                "root-background": "#e9df98",
+                "root-stroke": "#e9df98",
+                "root-font-size": 24,
+                "root-padding": compact ? [ 5, 10 ] : [ 15, 25 ],
+                "root-margin": compact ? 15 : 30,
+                "root-radius": 5,
+                "root-space": 10,
+                "root-shadow": "rgba(0, 0, 0, .25)",
+                "main-color": "#333",
+                "main-background": "#a4c5c0",
+                "main-stroke": "#a4c5c0",
+                "main-font-size": 16,
+                "main-padding": compact ? [ 4, 10 ] : [ 6, 20 ],
+                "main-margin": compact ? [ 5, 10 ] : [ 20, 40 ],
+                "main-radius": 5,
+                "main-space": 5,
+                "main-shadow": "rgba(0, 0, 0, .25)",
+                "sub-color": "black",
+                "sub-background": "white",
+                "sub-stroke": "white",
+                "sub-font-size": 12,
+                "sub-padding": [ 5, 10 ],
+                "sub-margin": compact ? [ 5, 10 ] : [ 10, 20 ],
+                "sub-radius": 5,
+                "sub-space": 5,
+                "connect-color": "white",
+                "connect-width": 2,
+                "main-connect-width": 3,
+                "connect-radius": 5,
+                "selected-background": "rgb(254, 219, 0)",
+                "selected-stroke": "rgb(254, 219, 0)",
+                "marquee-background": "rgba(255,255,255,.3)",
+                "marquee-stroke": "white",
+                "drop-hint-color": "yellow",
+                "drop-hint-width": 4,
+                "order-hint-area-color": "rgba(0, 255, 0, .5)",
+                "order-hint-path-color": "#0f0",
+                "order-hint-path-width": 1,
+                "text-selection-color": "rgb(27,171,255)",
+                "line-height": 1.5
+            });
+        });
+    }
+};
+
+//src/theme/tianpan.js
+_p[79] = {
+    value: function(require, exports, module) {
+        var theme = _p.r(32);
+        [ "tianpan", "tianpan-compact" ].forEach(function(name) {
+            var compact = name == "tianpan-compact";
+            theme.register(name, {
+                background: '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+                "root-color": "#430",
+                "root-background": "#e9df98",
+                "root-stroke": "#e9df98",
+                "root-font-size": 25,
+                "root-padding": compact ? 15 : 20,
+                "root-margin": compact ? [ 15, 25 ] : 100,
+                "root-radius": 30,
+                "root-space": 10,
+                "root-shadow": "rgba(0, 0, 0, .25)",
+                "root-shape": "circle",
+                "main-color": "#333",
+                "main-background": "#a4c5c0",
+                "main-stroke": "#a4c5c0",
+                "main-font-size": 15,
+                "main-padding": compact ? 10 : 12,
+                "main-margin": compact ? 10 : 12,
+                "main-radius": 10,
+                "main-space": 5,
+                "main-shadow": "rgba(0, 0, 0, .25)",
+                "main-shape": "circle",
+                "sub-color": "#333",
+                "sub-background": "#99ca6a",
+                "sub-stroke": "#a4c5c0",
+                "sub-font-size": 13,
+                "sub-padding": 5,
+                "sub-margin": compact ? 6 : 10,
+                "sub-tree-margin": 30,
+                "sub-radius": 5,
+                "sub-space": 5,
+                "sub-shadow": "rgba(0, 0, 0, .25)",
+                "sub-shape": "circle",
+                "connect-color": "white",
+                "connect-width": 2,
+                "main-connect-width": 3,
+                "connect-radius": 5,
+                "selected-background": "rgb(254, 219, 0)",
+                "selected-stroke": "rgb(254, 219, 0)",
+                "selected-color": "black",
+                "marquee-background": "rgba(255,255,255,.3)",
+                "marquee-stroke": "white",
+                "drop-hint-color": "yellow",
+                "sub-drop-hint-width": 2,
+                "main-drop-hint-width": 4,
+                "root-drop-hint-width": 4,
+                "order-hint-area-color": "rgba(0, 255, 0, .5)",
+                "order-hint-path-color": "#0f0",
+                "order-hint-path-width": 1,
+                "text-selection-color": "rgb(27,171,255)",
+                "line-height": 1.4
+            });
+        });
+    }
+};
+
+//src/theme/wire.js
+_p[80] = {
+    value: function(require, exports, module) {
+        var theme = _p.r(32);
+        theme.register("wire", {
+            background: "black",
+            color: "#999",
+            stroke: "none",
+            padding: 10,
+            margin: 20,
+            "font-size": 14,
+            "connect-color": "#999",
+            "connect-width": 1,
+            "selected-background": "#999",
+            "selected-color": "black",
+            "marquee-background": "rgba(255,255,255,.3)",
+            "marquee-stroke": "white",
+            "drop-hint-color": "yellow",
+            "sub-drop-hint-width": 2,
+            "main-drop-hint-width": 4,
+            "root-drop-hint-width": 4,
+            "order-hint-area-color": "rgba(0, 255, 0, .5)",
+            "order-hint-path-color": "#0f0",
+            "order-hint-path-width": 1,
+            "text-selection-color": "rgb(27,171,255)",
+            "line-height": 1.5
+        });
+    }
+};
+
+var moduleMapping = {
+    "expose-kityminder": 34
+};
+
+function use(name) {
+    _p.r([ moduleMapping[name] ]);
+}
+use('expose-kityminder');
+})();

Разлика између датотеке није приказан због своје велике величине
+ 9 - 0
kityminder-editor/dist/kityminder.core.min.js


+ 316 - 0
kityminder-editor/doc/Architecture.md

@@ -0,0 +1,316 @@
+# Kity Minder 目标设定
+
+## 第一期目标
+
+* 架构搭建
+* 自动布局
+* 双击节点文字编辑
+* 节点之间的连线(子节点向父节点)
+* 导入和导出(JSON格式)
+* 基本键盘导航:基于位置的选择导航;Tab插入子级;Enter插入同级
+* 上述功能的撤销操作
+
+## 第二期目标
+
+* 自由布局
+* 拖动修改子级
+* 子节点展开/收起
+* 思路连接线
+* 外观模板支持
+
+## 第三期目标
+
+* 协同操作
+* 云存储
+* 应用推广
+
+
+# Kity Minder 整体设计
+
+## `namespace` KityMinder
+暴露的命名空间,所有公开类都会放在该命名空间下。还会暴露一个简写的命名空间:KM。
+
+## `abstract` Command
+
+表示一条在 KityMinder 上执行的命令,以class的方式定义,命令必须依附于模块,不允许单独存在。
+
+## 命令定义结构:
+
+```js
+var MyCommand = kity.createClass({
+    base: Command,
+
+    execute: function(Minder minder [,args...]){},
+    revert: function(Minder minder){},
+
+    // 基类缺省实现:
+    queryState: function(Minder minder){},
+    queryValue: function(Minder minder){},
+
+    // 基类实现:
+    setContentChanged: function( bool ),
+    isContentChanged: function() {},
+
+    setSelectionChanged: function( bool ) {},
+    isSelectionChanged: function() {}
+}
+```
+
+### `method` execute(Minder minder [,args...] )
+定义command执行时的一些操作,不可缺省
+
+### `method` revert(Minder minder)
+定义revert操作,可缺省,如果没有则为不可revert
+
+### `method` queryState(Minder minder)
+todo:用于返回当前命令的state,分为
+
+* -1:不可执行
+* 0:可执行
+* 1:已执行
+
+可缺省,默认返回0
+
+### `method` queryValue(Minder minder)
+todo:用于返回当前命令的状态相关值,(例如:进度条的进度百分比值等)
+可缺省
+
+### `method` isContentChanged()
+返回命令是否对内容产生影响(true/false)
+缺省为 true
+
+### `method` isContentChanged()
+返回命令是否对选区产生影响(true/false)
+缺省为 false
+
+## Module
+Module定义一个模块,表示控制脑图中一个功能的模块(布局、渲染、输入文字、图标叠加等)
+
+### 模块定义
+下面代码简单展示了模块的定义方式
+
+```js
+KityMinder.registerModule("ModuleName", function() {
+    // 此处可以进行命令的定义、设置模块常量、工具函数等
+    return {
+
+        // 模块可能使用到的配置项,提供一个默认值
+    	"defaultOpitons": {
+    		
+    	},
+    	
+        // Minder 实例化的时候会调用 init 方法,this 指向正在实例化的 Minder 对象
+        // options 是 Minder 对象最终的配置(经过配置文件和用户设定改写)
+        "init": function( options ){
+
+        },
+        
+        // 注册模块需要使用到的命令
+        "commands": {
+            "mycommand": CommandClass
+        },
+        
+        // 模块需要关注的事件处理函数
+        // 处理函数中 this 指向事件发生的 Minder 对象
+        // e 参数为 MinderEvent 对象
+        "events": {
+            "click": function(e){
+            
+            },
+            "keydown keyup": function(e){
+            
+            }
+        },
+        
+        // Minder 被卸载的时候会调用 destroy 方法,模块自行回收自己的资源(事件由 Minder 自动回收)
+        // destroy 方法中的 this 指向 Minder 实例
+        "destroy": function() {
+        
+        },
+        
+        // Minder 被重设是会调用 reset 方法,模块自行
+        // reset 方法中的 this 指向 Minder 实例
+        "reset" : function() {
+        
+        }
+
+    }
+});
+```
+
+
+
+
+## MinderNode
+
+MinderNode 是 Kity Minder 需要展示和控制的树的一个节点。节点上提供了*树遍历*和*数据存取*的功能。并且提供对节点对应的渲染容器(Kity.Group)的访问
+
+节点上提供公开字段,任何模块可以读取和修改,这些字段会提供给 KityMinder 作为渲染的依据。
+
+公开的字段和存放的数据都会在导出、保存现场的时候被保留。
+
+MinderTreeNode 维护的树关系和数据只是作为一个脑图的结构和数据,不具有任何渲染和交互的能力。
+
+### 树遍历
+
+通过 6 个接口来进行树的访问和修改操作
+
+`node.getParent()` 返回当前节点的父节点
+
+`node.getChildren()` 返回当前节点的子节点
+
+`node.getIndex()` 返回当前节点在父节点中的位置,如果没有父节点,返回 -1
+
+`node.insertChild( node, index )` 插入一个子节点到当前节点上,插入的位置为 index
+
+`node.removeChild( node | index )` 移除一个子节点或指定位置的子节点
+
+`node.getChild( index )` 获得指定位置的子节点
+
+### 数据存取
+
+`node.getData(name)` 获得指定字段的数据
+
+`node.setData(name, value)` 设定指定字段的数据
+
+### 公开字段
+
+`node.setData( "x", value )` 设置节点的 x 坐标
+
+`node.setData( "y", value )` 设置节点的 y 坐标
+
+`node.getData( "x", value )` 获取节点的 x 坐标
+
+`node.getData( "y", value )` 获取节点的 y 坐标
+
+`node.setData( "text", value )` 设置节点的文本
+
+`node.getData( "text", value )` 获取节点的文本
+
+### 渲染容器
+
+`node.getRenderContainer()` 返回当前节点的渲染容器
+
+
+## Minder
+
+脑图使用类
+
+### `static method` registerModule( name, module )
+注册一个模块
+
+### 构造函数
+
+`constructor` KityMinder(id, option)
+
+创建脑图画布。KityMinder 实例化的时候,会从模块池中取出模块,并且实例化这些模块,然后加载。
+
+`id` 实例的 id
+
+`option` 其他选项(当前没有)
+
+### 公开接口
+
+`.getRoot() : MinderNode` 
+
+获取脑图根节点
+
+`.execCommand( name [, params...] ) : this`
+
+执行指定的命令。该方法执行的时候,会实例化指定类型的命令,并且把命令参数传给命令执行
+
+`.queryCommandState( name )`
+
+查询命令的当前状态
+
+`.queryCommandValue( name )`
+
+查询命令的当前结果
+
+`.update(MinderNode node) : this`
+
+更新指定节点及其子树的呈现,如果不指定节点,则更新跟节点的呈现(整棵树)
+
+`.export() : object`
+
+以导出节点以及所有子树的数据(data上所有的字段会被导出)
+
+`.import(object data) : this`
+
+导入节点数据,会节点以及所有子树结构以及子树数据
+
+`.getSelectedNodes() : MinderNode[]`
+
+返回选中的节点列表
+
+`.select(MinderNode[] nodes | MinderNode node) : this`
+
+添加一个或多个节点到节点选择列表中
+
+`.selectSingle(Minder node) : this`
+
+唯一选中指定节点
+
+`.toggleSelect(MinderNode[] nodes | MinderNode node)`
+
+切换一个或多个节点的选中状态
+
+`.clearSelect(MinderNode[] nodes | MinderNode node) : this`
+
+从节点选择列表中移除一个或多个节点,如果不传节点,全部取消选择
+
+### 事件机制
+
+#### 事件分类
+
+KityMinder 的事件分为:
+
+* 交互事件: `click`, `dblclick`, `mousedown`, `mousemove`, `mouseup`, `keydown`, `keyup`, `keypress`, `touchstart`, `touchend`, `touchmove`
+
+* Command 事件: `beforecommand`, `precommand`, `aftercommand`
+
+* 交互事件:`selectionchange`, `contentchange`, `interactchange`
+
+* 模块事件:模块自行触发与上述不同名的事件
+
+#### 事件接口
+
+`.on(event, callback)` 侦听指定事件
+
+`.once(event, callback)` 侦听指定事件一次,当 callback 被调用之后,后面再发生该事件不再被调用
+
+`.off(event, callback)` 取消对事件的侦听
+
+`.fire(event, params)` 触发指定的事件,params 是自定义的 JSON 数据,会合并到事件对象
+
+
+#### 回调函数接口
+
+回调函数接收唯一的参数 e
+
+对于交互事件,e 是原生 event 对象的一个拓展;对于需要坐标的事件,用 e.getPosition() 可以获得在 Kity Paper 上的坐标值
+
+对 command 事件:
+
+* `e.commandName` 获取执行的命令的类型
+* `e.commandArgs` 获取命令执行的参数列表
+
+对 import 事件:
+
+* `e.getImportData()` 获取导入的数据
+
+对 selectionchange 事件:
+
+* `e.currentSelection` 获取当前选择的节点列表
+* `e.additionNodes` 添加到选择节点列表的那部分节点
+* `e.removalNodes` 从选择节点列表移除的那部分节点
+
+#### 事件触发时机
+
+`command` 事件只在顶级 command 执行的时候触发(Command 里调用 Command 不触发)
+
+`contentchange` 事件在顶级 command 之后会查询是否发生了内容的变化,如果发生了变化,则会触发;
+
+`selectionchange` 事件在顶级 command 之后会查询是否发生了选区的变化,如果发生了变化,则会触发
+
+`interactchange` 事件会在所有的鼠标、键盘、触摸操作后发生,并且会进行稀释;command 可以手动触发该事件,此时不会被稀释

+ 65 - 0
kityminder-editor/example.html

@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>KityMinder Example</title>
+    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" >
+    <link href="favicon.ico" type="image/x-icon" rel="shortcut icon">
+    <link rel="stylesheet" href="src/kityminder.css" rel="stylesheet">
+
+    <style type="text/css">
+        body {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+        }
+
+        #minder-view {
+            position: absolute;
+            border: 1px solid #ccc;
+            left: 10px;
+            top: 10px;
+            bottom: 10px;
+            right: 10px;
+        }
+    </style>
+
+    <script type="text/javascript" src="node_modules/kity/dist/kity.min.js"></script>
+</head>
+<body>
+    <script id="minder-view" type="application/kityminder" minder-data-type="json">
+    {
+        "root": {
+            "data": {
+                "text": "百度产品",
+                "image": "https://www.baidu.com/img/bd_logo1.png?where=super",
+                "imageSize": { "width": 270, "height": 129 }
+            },
+            "children": [
+                { "data": { "text": "新闻" } },
+                { "data": { "text": "网页", "priority": 1 } },
+                { "data": { "text": "贴吧", "priority": 2 } },
+                { "data": { "text": "知道", "priority": 2 } },
+                { "data": { "text": "音乐", "priority": 3 } },
+                { "data": { "text": "图片", "priority": 3 } },
+                { "data": { "text": "视频", "priority": 3 } },
+                { "data": { "text": "地图", "priority": 3 } },
+                { "data": { "text": "百科", "priority": 3 } },
+                { "data": { "text": "更多", "hyperlink": "http://www.baidu.com/more" } }
+            ]
+        }
+    }
+    </script>
+</body>
+
+<script type="text/javascript" src="dist/kityminder.core.min.js"></script>
+<script type="text/javascript">
+    // 创建 km 实例
+    /* global kityminder */
+    var km = window.km = new kityminder.Minder();
+    km.setup('#minder-view');
+    km.disable();
+    km.execCommand('hand');
+</script>
+
+</html>

BIN
kityminder-editor/icon.ico


+ 101 - 0
kityminder-editor/import.js

@@ -0,0 +1,101 @@
+
+
+/**
+ * 开发版本的文件导入
+ */
+(function() {
+    /* 可能的文件路径,已按照依赖关系排序 */
+    var pathInfo = [
+        /* 核心代码 */
+        'src/core/kityminder.js',
+        'src/core/utils.js',
+
+        'src/core/command.js',
+        'src/core/node.js',
+
+        'src/core/options.js',
+        'src/core/event.js',
+        'src/core/status.js',
+        'src/core/paper.js',
+        'src/core/select.js',
+        'src/core/module.js',
+        'src/core/data.js',
+        'src/core/readonly.js',
+        'src/core/layout.js',
+        'src/core/theme.js',
+        
+        'src/core/compatibility.js',
+        'src/core/render.js',
+        'src/core/connect.js',
+        'src/core/template.js',
+        'src/core/keymap.js',
+
+        /* 布局 */
+        'src/layout/mind.js',
+        'src/layout/filetree.js',
+        'src/layout/btree.js',
+        'src/layout/fish-bone-master.js',
+        'src/layout/fish-bone-slave.js',
+
+        /* 连线 */
+        'src/connect/bezier.js',
+        'src/connect/poly.js',
+        'src/connect/arc.js',
+        'src/connect/under.js',
+        'src/connect/l.js',
+        'src/connect/fish-bone-master.js',
+
+        /* 皮肤 */
+        'src/theme/default.js',
+        'src/theme/snow.js',
+        'src/theme/fresh.js',
+        'src/theme/fish.js',
+        'src/theme/wire.js',
+
+        /* 模板 */
+        'src/template/default.js',
+        'src/template/structure.js',
+        'src/template/filetree.js',
+        'src/template/right.js',
+        'src/template/fish-bone.js',
+
+        /* 模块 */
+        'src/module/node.js',
+        'src/module/text.js',
+        'src/module/expand.js',
+        'src/module/outline.js',
+        'src/module/history.js',
+        'src/module/progress.js',
+        'src/module/priority.js',
+        'src/module/image.js',
+        'src/module/resource.js',
+        'src/module/view.js',
+        'src/module/dragtree.js',
+        'src/module/keynav.js',
+        'src/module/select.js',
+        'src/module/history.js',
+        'src/module/editor.js',
+        'src/module/editor.keyboard.js',
+        'src/module/editor.range.js',
+        'src/module/editor.receiver.js',
+        'src/module/editor.selection.js',
+        'src/module/basestyle.js',
+        'src/module/font.js',
+        'src/module/zoom.js',
+        'src/module/hyperlink.js',
+        'src/module/arrange.js',
+        'src/module/clipboard.js',
+        'src/module/style.js'
+    ];
+
+    if (typeof(module) === 'object' && module.exports) {
+        module.exports = pathInfo;
+    } 
+
+    else if (document) {
+        while (pathInfo.length) {
+            var path = pathInfo.shift();
+            window.document.write('<script type="text/javascript" src="' + path + '"></script>');
+        }
+    }
+})();

+ 828 - 1
kityminder-editor/node_modules/.package_versions.json

@@ -1 +1,828 @@
-{}
+{
+  "cz-conventional-changelog": [
+    "1.2.0"
+  ],
+  "grunt-contrib-concat": [
+    "0.5.1"
+  ],
+  "grunt-replace": [
+    "0.8.0"
+  ],
+  "grunt-contrib-watch": [
+    "1.1.0"
+  ],
+  "json-diff": [
+    "0.5.5"
+  ],
+  "grunt-contrib-copy": [
+    "0.8.2"
+  ],
+  "grunt-browser-sync": [
+    "2.2.0"
+  ],
+  "grunt-contrib-clean": [
+    "0.5.0"
+  ],
+  "grunt-contrib-uglify": [
+    "0.4.1"
+  ],
+  "grunt-module-dependence": [
+    "0.2.1"
+  ],
+  "grunt": [
+    "0.4.5"
+  ],
+  "uglify-js": [
+    "2.8.29"
+  ],
+  "pad-right": [
+    "0.2.2"
+  ],
+  "word-wrap": [
+    "1.2.3"
+  ],
+  "chalk": [
+    "0.5.1",
+    "0.4.0",
+    "1.1.3"
+  ],
+  "right-pad": [
+    "1.0.1"
+  ],
+  "conventional-commit-types": [
+    "2.3.0"
+  ],
+  "lodash.map": [
+    "4.6.0"
+  ],
+  "longest": [
+    "1.0.1"
+  ],
+  "gaze": [
+    "1.1.3"
+  ],
+  "applause": [
+    "0.3.4"
+  ],
+  "async": [
+    "0.1.22",
+    "2.6.4"
+  ],
+  "lodash": [
+    "0.9.2",
+    "2.4.2",
+    "4.17.21"
+  ],
+  "eventemitter2": [
+    "0.4.14"
+  ],
+  "dateformat": [
+    "1.0.2-1.2.3"
+  ],
+  "which": [
+    "1.0.9"
+  ],
+  "ansi-styles": [
+    "1.1.0",
+    "1.0.0",
+    "2.2.1",
+    "4.3.0"
+  ],
+  "maxmin": [
+    "0.1.0"
+  ],
+  "nopt": [
+    "1.0.10",
+    "3.0.6"
+  ],
+  "rimraf": [
+    "2.2.8"
+  ],
+  "repeat-string": [
+    "1.6.1"
+  ],
+  "escape-string-regexp": [
+    "1.0.5"
+  ],
+  "strip-ansi": [
+    "0.3.0",
+    "3.0.1",
+    "0.1.1",
+    "6.0.1"
+  ],
+  "colors": [
+    "0.6.2"
+  ],
+  "has-ansi": [
+    "0.1.0",
+    "2.0.0"
+  ],
+  "supports-color": [
+    "2.0.0",
+    "0.2.0"
+  ],
+  "dreamopt": [
+    "0.6.0"
+  ],
+  "has-color": [
+    "0.1.7"
+  ],
+  "uglify-to-browserify": [
+    "1.0.2"
+  ],
+  "cli-color": [
+    "0.1.7"
+  ],
+  "difflib": [
+    "0.2.4"
+  ],
+  "minimatch": [
+    "0.2.14",
+    "3.0.8",
+    "3.1.2",
+    "0.3.0"
+  ],
+  "getobject": [
+    "0.1.0"
+  ],
+  "hooker": [
+    "0.2.3"
+  ],
+  "grunt-legacy-log": [
+    "0.1.3"
+  ],
+  "findup-sync": [
+    "0.1.3"
+  ],
+  "yargs": [
+    "3.10.0",
+    "17.5.1",
+    "17.1.1"
+  ],
+  "globule": [
+    "1.3.3"
+  ],
+  "grunt-legacy-util": [
+    "0.2.0"
+  ],
+  "glob": [
+    "3.1.21",
+    "7.1.7",
+    "3.2.11"
+  ],
+  "file-sync-cmp": [
+    "0.1.1"
+  ],
+  "exit": [
+    "0.1.2"
+  ],
+  "ansi-regex": [
+    "0.2.1",
+    "2.1.1",
+    "5.0.1"
+  ],
+  "abbrev": [
+    "1.1.1"
+  ],
+  "source-map": [
+    "0.5.7",
+    "0.3.0"
+  ],
+  "coffee-script": [
+    "1.3.3",
+    "1.8.0",
+    "1.7.1"
+  ],
+  "cson": [
+    "1.6.2"
+  ],
+  "iconv-lite": [
+    "0.2.11",
+    "0.4.24"
+  ],
+  "sigmund": [
+    "1.0.1"
+  ],
+  "decamelize": [
+    "1.2.0"
+  ],
+  "window-size": [
+    "0.1.0"
+  ],
+  "camelcase": [
+    "1.2.1"
+  ],
+  "wordwrap": [
+    "1.0.0",
+    "0.0.2"
+  ],
+  "pretty-bytes": [
+    "0.1.2"
+  ],
+  "lru-cache": [
+    "2.7.3"
+  ],
+  "underscore.string": [
+    "2.2.1",
+    "2.3.3",
+    "2.4.0"
+  ],
+  "cliui": [
+    "2.1.0",
+    "7.0.4"
+  ],
+  "grunt-legacy-log-utils": [
+    "0.1.1"
+  ],
+  "gzip-size": [
+    "0.1.1"
+  ],
+  "heap": [
+    "0.2.7"
+  ],
+  "inherits": [
+    "1.0.2",
+    "2.0.4",
+    "2.0.3"
+  ],
+  "tiny-lr": [
+    "1.1.1"
+  ],
+  "brace-expansion": [
+    "1.1.11"
+  ],
+  "graceful-fs": [
+    "1.2.3",
+    "4.2.10"
+  ],
+  "js-yaml": [
+    "2.0.5",
+    "3.14.1"
+  ],
+  "once": [
+    "1.4.0"
+  ],
+  "path-is-absolute": [
+    "1.0.1"
+  ],
+  "inflight": [
+    "1.0.6"
+  ],
+  "fs.realpath": [
+    "1.0.0"
+  ],
+  "concat-stream": [
+    "1.6.2"
+  ],
+  "amdefine": [
+    "1.0.1"
+  ],
+  "center-align": [
+    "0.1.3"
+  ],
+  "right-align": [
+    "0.1.3"
+  ],
+  "requirefresh": [
+    "1.1.2"
+  ],
+  "extract-opts": [
+    "2.2.0"
+  ],
+  "object-assign": [
+    "4.1.1"
+  ],
+  "balanced-match": [
+    "1.0.2"
+  ],
+  "debug": [
+    "3.2.7",
+    "4.3.2",
+    "2.6.9",
+    "4.3.4"
+  ],
+  "concat-map": [
+    "0.0.1"
+  ],
+  "ambi": [
+    "2.5.0"
+  ],
+  "wrappy": [
+    "1.0.2"
+  ],
+  "body": [
+    "5.1.0"
+  ],
+  "buffer-from": [
+    "1.1.2"
+  ],
+  "zlib-browserify": [
+    "0.0.3"
+  ],
+  "livereload-js": [
+    "2.4.0"
+  ],
+  "faye-websocket": [
+    "0.10.0"
+  ],
+  "esprima": [
+    "4.0.1",
+    "1.0.4"
+  ],
+  "qs": [
+    "6.10.3",
+    "6.2.3"
+  ],
+  "lazy-cache": [
+    "1.0.4"
+  ],
+  "align-text": [
+    "0.1.4"
+  ],
+  "typedarray": [
+    "0.0.6"
+  ],
+  "ms": [
+    "2.1.3",
+    "2.0.0",
+    "2.1.2"
+  ],
+  "typechecker": [
+    "2.0.8",
+    "4.11.0"
+  ],
+  "editions": [
+    "1.3.4",
+    "2.3.1"
+  ],
+  "js2coffee": [
+    "0.3.5"
+  ],
+  "raw-body": [
+    "1.1.7",
+    "2.5.1"
+  ],
+  "argparse": [
+    "1.0.10",
+    "0.1.16"
+  ],
+  "readable-stream": [
+    "2.3.7"
+  ],
+  "continuable-cache": [
+    "0.3.1"
+  ],
+  "kind-of": [
+    "3.2.2"
+  ],
+  "safe-json-parse": [
+    "1.0.1"
+  ],
+  "error": [
+    "7.2.1"
+  ],
+  "side-channel": [
+    "1.0.4"
+  ],
+  "string_decoder": [
+    "0.10.31",
+    "1.1.1"
+  ],
+  "bytes": [
+    "1.0.0",
+    "3.1.2"
+  ],
+  "websocket-driver": [
+    "0.7.4"
+  ],
+  "mkdirp": [
+    "0.3.5"
+  ],
+  "core-util-is": [
+    "1.0.3"
+  ],
+  "safe-buffer": [
+    "5.1.2",
+    "5.2.1"
+  ],
+  "process-nextick-args": [
+    "2.0.1"
+  ],
+  "util-deprecate": [
+    "1.0.2"
+  ],
+  "isarray": [
+    "1.0.0"
+  ],
+  "is-buffer": [
+    "1.1.6"
+  ],
+  "sprintf-js": [
+    "1.0.3"
+  ],
+  "underscore": [
+    "1.6.0",
+    "1.7.0"
+  ],
+  "string-template": [
+    "0.2.1"
+  ],
+  "get-intrinsic": [
+    "1.1.1"
+  ],
+  "call-bind": [
+    "1.0.2"
+  ],
+  "file": [
+    "0.2.2"
+  ],
+  "browser-sync": [
+    "2.27.10"
+  ],
+  "http-parser-js": [
+    "0.5.6"
+  ],
+  "tape": [
+    "0.2.2"
+  ],
+  "websocket-extensions": [
+    "0.1.4"
+  ],
+  "has": [
+    "1.0.3"
+  ],
+  "semver": [
+    "6.3.0"
+  ],
+  "object-inspect": [
+    "1.12.1"
+  ],
+  "errlop": [
+    "2.2.0"
+  ],
+  "function-bind": [
+    "1.1.1"
+  ],
+  "etag": [
+    "1.8.1"
+  ],
+  "has-symbols": [
+    "1.0.3"
+  ],
+  "fresh": [
+    "0.5.2"
+  ],
+  "connect-history-api-fallback": [
+    "1.6.0"
+  ],
+  "bs-snippet-injector": [
+    "2.0.1"
+  ],
+  "easy-extender": [
+    "2.3.4"
+  ],
+  "eazy-logger": [
+    "3.1.0"
+  ],
+  "browser-sync-client": [
+    "2.27.10"
+  ],
+  "opn": [
+    "5.3.0"
+  ],
+  "send": [
+    "0.16.2"
+  ],
+  "connect": [
+    "3.6.6"
+  ],
+  "micromatch": [
+    "4.0.5"
+  ],
+  "resp-modifier": [
+    "6.0.2"
+  ],
+  "portscanner": [
+    "2.2.0"
+  ],
+  "server-destroy": [
+    "1.0.1"
+  ],
+  "serve-static": [
+    "1.13.2"
+  ],
+  "chokidar": [
+    "3.5.3"
+  ],
+  "unpipe": [
+    "1.0.0"
+  ],
+  "http-errors": [
+    "2.0.0",
+    "1.6.3"
+  ],
+  "jsonify": [
+    "0.0.0"
+  ],
+  "dev-ip": [
+    "1.0.1"
+  ],
+  "localtunnel": [
+    "2.0.2"
+  ],
+  "deep-equal": [
+    "0.0.0"
+  ],
+  "defined": [
+    "0.0.0"
+  ],
+  "immutable": [
+    "3.8.2"
+  ],
+  "tfunk": [
+    "4.0.0"
+  ],
+  "is-wsl": [
+    "1.1.0"
+  ],
+  "http-proxy": [
+    "1.18.1"
+  ],
+  "escape-html": [
+    "1.0.3"
+  ],
+  "destroy": [
+    "1.0.4"
+  ],
+  "encodeurl": [
+    "1.0.2"
+  ],
+  "on-finished": [
+    "2.3.0"
+  ],
+  "statuses": [
+    "1.4.0",
+    "2.0.1",
+    "1.5.0",
+    "1.3.1"
+  ],
+  "range-parser": [
+    "1.2.1"
+  ],
+  "depd": [
+    "2.0.0",
+    "1.1.2"
+  ],
+  "parseurl": [
+    "1.3.3"
+  ],
+  "normalize-path": [
+    "3.0.0"
+  ],
+  "mime": [
+    "1.4.1"
+  ],
+  "utils-merge": [
+    "1.0.1"
+  ],
+  "mitt": [
+    "1.2.0"
+  ],
+  "finalhandler": [
+    "1.1.0"
+  ],
+  "is-binary-path": [
+    "2.1.0"
+  ],
+  "glob-parent": [
+    "5.1.2"
+  ],
+  "anymatch": [
+    "3.1.2"
+  ],
+  "is-glob": [
+    "4.0.3"
+  ],
+  "setprototypeof": [
+    "1.1.0",
+    "1.2.0"
+  ],
+  "ua-parser-js": [
+    "1.0.2"
+  ],
+  "readdirp": [
+    "3.6.0"
+  ],
+  "toidentifier": [
+    "1.0.1"
+  ],
+  "is-number-like": [
+    "1.0.8"
+  ],
+  "picomatch": [
+    "2.3.1"
+  ],
+  "braces": [
+    "3.0.2"
+  ],
+  "openurl": [
+    "1.1.1"
+  ],
+  "ee-first": [
+    "1.1.1"
+  ],
+  "requires-port": [
+    "1.0.0"
+  ],
+  "eventemitter3": [
+    "4.0.7"
+  ],
+  "follow-redirects": [
+    "1.15.0"
+  ],
+  "dlv": [
+    "1.1.3"
+  ],
+  "binary-extensions": [
+    "2.2.0"
+  ],
+  "socket.io": [
+    "4.5.1"
+  ],
+  "is-extglob": [
+    "2.1.1"
+  ],
+  "safer-buffer": [
+    "2.1.2"
+  ],
+  "lodash.isfinite": [
+    "3.3.2"
+  ],
+  "fill-range": [
+    "7.0.1"
+  ],
+  "fs-extra": [
+    "3.0.1"
+  ],
+  "accepts": [
+    "1.3.8"
+  ],
+  "string-width": [
+    "4.2.3"
+  ],
+  "socket.io-adapter": [
+    "2.4.0"
+  ],
+  "base64id": [
+    "2.0.0"
+  ],
+  "require-directory": [
+    "2.1.1"
+  ],
+  "get-caller-file": [
+    "2.0.5"
+  ],
+  "y18n": [
+    "5.0.8"
+  ],
+  "escalade": [
+    "3.1.1"
+  ],
+  "axios": [
+    "0.21.4"
+  ],
+  "socket.io-parser": [
+    "4.0.4",
+    "4.2.0"
+  ],
+  "to-regex-range": [
+    "5.0.1"
+  ],
+  "yargs-parser": [
+    "21.0.1",
+    "20.2.9"
+  ],
+  "wrap-ansi": [
+    "7.0.0"
+  ],
+  "universalify": [
+    "0.1.2"
+  ],
+  "is-fullwidth-code-point": [
+    "3.0.0"
+  ],
+  "mime-types": [
+    "2.1.35"
+  ],
+  "jsonfile": [
+    "3.0.1"
+  ],
+  "negotiator": [
+    "0.6.3"
+  ],
+  "serve-index": [
+    "1.9.1"
+  ],
+  "emoji-regex": [
+    "8.0.0"
+  ],
+  "engine.io": [
+    "6.2.0"
+  ],
+  "component-emitter": [
+    "1.3.0"
+  ],
+  "is-number": [
+    "7.0.0"
+  ],
+  "@types/component-emitter": [
+    "1.2.11"
+  ],
+  "mime-db": [
+    "1.52.0"
+  ],
+  "color-convert": [
+    "2.0.1"
+  ],
+  "es5-ext": [
+    "0.8.2"
+  ],
+  "cookie": [
+    "0.4.2"
+  ],
+  "batch": [
+    "0.6.1"
+  ],
+  "@types/cookie": [
+    "0.4.1"
+  ],
+  "@types/cors": [
+    "2.8.12"
+  ],
+  "cors": [
+    "2.8.5"
+  ],
+  "color-name": [
+    "1.1.4"
+  ],
+  "kity": [
+    "2.0.4"
+  ],
+  "ws": [
+    "8.2.3"
+  ],
+  "engine.io-parser": [
+    "5.0.4"
+  ],
+  "browser-sync-ui": [
+    "2.27.10"
+  ],
+  "vary": [
+    "1.1.2"
+  ],
+  "@types/node": [
+    "17.0.35"
+  ],
+  "async-each-series": [
+    "0.1.1"
+  ],
+  "stream-throttle": [
+    "0.1.3"
+  ],
+  "commander": [
+    "2.20.3"
+  ],
+  "bs-recipes": [
+    "1.3.4"
+  ],
+  "limiter": [
+    "1.1.5"
+  ],
+  "socket.io-client": [
+    "4.5.1"
+  ],
+  "@socket.io/component-emitter": [
+    "3.1.0"
+  ],
+  "rx": [
+    "4.1.0"
+  ],
+  "seajs": [
+    "2.3.0"
+  ],
+  "engine.io-client": [
+    "6.2.2"
+  ],
+  "xmlhttprequest-ssl": [
+    "2.0.0"
+  ],
+  "typescript": [
+    "4.6.4"
+  ],
+  "rxjs": [
+    "5.5.12"
+  ],
+  "symbol-observable": [
+    "1.0.1"
+  ]
+}

Разлика између датотеке није приказан због своје велике величине
+ 139 - 565
kityminder-editor/package-lock.json


+ 38 - 36
kityminder-editor/package.json

@@ -1,59 +1,61 @@
 {
-  "name": "kityminder-editor",
-  "version": "1.0.67",
-  "description": "A powerful mind map editor",
-  "main": "kityminder.editor.js",
-  "scripts": {
-    "init": "npm i -g wr && npm install -g less && npm install -g bower && bower install && npm install",
-    "build": "grunt build",
-    "dev": "grunt dev",
-    "watch": "wr --exec \"lessc --source-map less/editor.less dist/kityminder.editor.css && grunt build\" less ui",
-    "postinstall": "bower install"
+  "name": "kityminder-core",
+  "title": "Kity Minder Core",
+  "description": "KityMinder Core Implement",
+  "version": "1.4.50",
+  "homepage": "https://github.com/fex-team/kityminder-core",
+  "author": {
+    "name": "Baidu FEX",
+    "url": "http://fex.baidu.com"
   },
+  "main": "dist/kityminder.core.js",
+  "license": "BSD-3-Clause",
   "repository": {
     "type": "git",
-    "url": "https://github.com/fex-team/kityminder-editor"
+    "url": "https://github.com/fex-team/kityminder-core.git"
   },
   "keywords": [
     "kityminder",
-    "editor",
-    "html5",
-    "js",
-    "mindmap"
+    "kity",
+    "minder",
+    "svg",
+    "graphic",
+    "javascript"
   ],
-  "author": "fex <fex@baidu.com>",
-  "license": "GPL-2.0",
+  "scripts": {
+    "dev": "grunt dev",
+    "build": "grunt build"
+  },
   "bugs": {
-    "url": "https://github.com/fex-team/kityminder-editor/issues"
+    "url": "https://github.com/fex-team/kityminder-core/issues"
+  },
+  "licenses": [
+    {
+      "type": "BSD-3-Clause",
+      "url": "https://github.com/fex-team/kityminder-core/blob/dev/LICENSE"
+    }
+  ],
+  "dependencies": {
+    "json-diff": "^0.5.2",
+    "kity": "^2.0.4",
+    "seajs": "^2.3.0"
   },
-  "homepage": "https://github.com/fex-team/kityminder-editor",
   "devDependencies": {
     "cz-conventional-changelog": "^1.1.5",
     "grunt": "~0.4.1",
-    "grunt-angular-templates": "~0.5.0",
     "grunt-browser-sync": "^2.2.0",
-    "grunt-contrib-clean": "^0.5.0",
+    "grunt-contrib-clean": "~0.5.0",
     "grunt-contrib-concat": "~0.5.0",
-    "grunt-contrib-copy": "^0.5.0",
-    "grunt-contrib-cssmin": "^0.12.0",
-    "grunt-contrib-less": "^1.0.0",
-    "grunt-contrib-uglify": "^3.3.0",
+    "grunt-contrib-copy": "~0.8.2",
+    "grunt-contrib-uglify": "~0.4.0",
     "grunt-contrib-watch": "^1.0.0",
-    "grunt-module-dependence": "~0.2.0",
-    "grunt-ng-annotate": "^0.9.2",
+    "grunt-module-dependence": "~0.2.1",
     "grunt-replace": "~0.8.0",
-    "grunt-wiredep": "^2.0.0",
-    "jshint-stylish": "^1.0.0",
-    "load-grunt-tasks": "^3.1.0",
     "uglify-js": "^2.8.29"
   },
-  "dependencies": {
-    "kityminder-core": "^1.4.50"
-  },
   "config": {
     "commitizen": {
       "path": "./node_modules/cz-conventional-changelog"
     }
-  },
-  "__npminstall_done": false
-}
+  }
+}

+ 49 - 0
kityminder-editor/src/connect/arc.js

@@ -0,0 +1,49 @@
+/**
+ * @fileOverview
+ *
+ * 圆弧连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    var connectMarker = new kity.Marker().pipe(function() {
+        var r = 7;
+        var dot = new kity.Circle(r - 1);
+        this.addShape(dot);
+        this.setRef(r - 1, 0).setViewBox(-r, -r, r + r, r + r).setWidth(r).setHeight(r);
+        this.dot = dot;
+        this.node.setAttribute('markerUnits', 'userSpaceOnUse');
+    });
+
+    connect.register('arc', function(node, parent, connection, width, color) {
+
+        var box = node.getLayoutBox(),
+            pBox = parent.getLayoutBox();
+
+        var start, end, vector;
+        var abs = Math.abs;
+        var pathData = [];
+        var side = box.x > pBox.x ? 'right' : 'left';
+
+        node.getMinder().getPaper().addResource(connectMarker);
+
+        start = new kity.Point(pBox.cx, pBox.cy);
+        end = side == 'left' ?
+            new kity.Point(box.right + 2, box.cy) :
+            new kity.Point(box.left - 2, box.cy);
+
+        vector = kity.Vector.fromPoints(start, end);
+        pathData.push('M', start);
+        pathData.push('A', abs(vector.x), abs(vector.y), 0, 0, (vector.x * vector.y > 0 ? 0 : 1), end);
+
+        connection.setMarker(connectMarker);
+        connectMarker.dot.fill(color);
+
+        connection.setPathData(pathData);
+    });
+});

+ 85 - 0
kityminder-editor/src/connect/arc_tp.js

@@ -0,0 +1,85 @@
+/**
+ *
+ * 圆弧连线
+ *
+ * @author: along
+ * @copyright: bpd729@163.com , 2015
+ */
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    var connectMarker = new kity.Marker().pipe(function () {
+        var r = 7;
+        var dot = new kity.Circle(r - 1);
+        this.addShape(dot);
+        this.setRef(r - 1, 0).setViewBox(-r, -r, r + r, r + r).setWidth(r).setHeight(r);
+        this.dot = dot;
+        this.node.setAttribute('markerUnits', 'userSpaceOnUse');
+    });
+
+    /**
+     * 天盘图连线除了连接当前节点和前一个节点外, 还需要渲染当前节点和后一个节点的连接, 防止样式上的断线
+     * 这是天盘图与其余的模板不同的地方
+     */
+    connect.register('arc_tp', function (node, parent, connection, width, color) {
+        var end_box = node.getLayoutBox(),
+            start_box = parent.getLayoutBox();
+
+        var index = node.getIndex();
+        var nextNode = parent.getChildren()[index + 1];
+
+
+        if (node.getIndex() > 0) {
+            start_box = parent.getChildren()[index - 1].getLayoutBox();
+        }
+
+
+        var start, end, vector;
+        var abs = Math.abs;
+        var pathData = [];
+        var side = end_box.x > start_box.x ? 'right' : 'left';
+
+        node.getMinder().getPaper().addResource(connectMarker);
+
+
+        start = new kity.Point(start_box.cx, start_box.cy);
+        end = new kity.Point(end_box.cx, end_box.cy);
+
+        var jl = Math.sqrt(Math.pow((start.x - end.x), 2) + Math.pow((start.y - end.y), 2)); //两圆中心点距离
+
+        jl = node.getIndex() == 0 ? jl * 0.4 : jl;
+
+
+        vector = kity.Vector.fromPoints(start, end);
+        pathData.push('M', start);
+        pathData.push('A', jl, jl, 0, 0, 1, end);
+
+
+        connection.setMarker(connectMarker);
+        connectMarker.dot.fill(color);
+        connection.setPathData(pathData);
+
+
+        // 设置下一个的节点的连接线
+        if (nextNode && nextNode.getConnection()) {
+            var nextConnection = nextNode.getConnection();
+            var next_end_box = nextNode.getLayoutBox();
+            var next_end = new kity.Point(next_end_box.cx, next_end_box.cy);
+
+            var jl2 = Math.sqrt(Math.pow((end.x - next_end.x), 2) + Math.pow((end.y - next_end.y), 2)); //两圆中心点距离
+
+            pathData = [];
+
+            pathData.push('M', end);
+            pathData.push('A', jl2, jl2, 0, 0, 1, next_end);
+
+            nextConnection.setMarker(connectMarker);
+            connectMarker.dot.fill(color);
+
+            nextConnection.setPathData(pathData);
+
+        }
+
+    });
+});

+ 42 - 0
kityminder-editor/src/connect/bezier.js

@@ -0,0 +1,42 @@
+/**
+ * @fileOverview
+ *
+ * 提供折线相连的方法
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    connect.register('bezier', function(node, parent, connection) {
+
+        // 连线起点和终点
+        var po = parent.getLayoutVertexOut(),
+            pi = node.getLayoutVertexIn();
+
+        // 连线矢量和方向
+        var v = parent.getLayoutVectorOut().normalize();
+
+        var r = Math.round;
+        var abs = Math.abs;
+
+        var pathData = [];
+        pathData.push('M', r(po.x), r(po.y));
+
+        if (abs(v.x) > abs(v.y)) {
+            // x - direction
+            var hx = (pi.x + po.x) / 2;
+            pathData.push('C', hx, po.y, hx, pi.y, pi.x, pi.y);
+        } else {
+            // y - direction
+            var hy = (pi.y + po.y) / 2;
+            pathData.push('C', po.x, hy, pi.x, hy, pi.x, pi.y);
+        }
+
+        connection.setMarker(null);
+        connection.setPathData(pathData);
+    });
+});

+ 33 - 0
kityminder-editor/src/connect/fish-bone-master.js

@@ -0,0 +1,33 @@
+/**
+ * @fileOverview
+ *
+ * 鱼骨头主干连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    connect.register('fish-bone-master', function(node, parent, connection) {
+
+        var pout = parent.getLayoutVertexOut(),
+            pin = node.getLayoutVertexIn();
+
+        var abs = Math.abs;
+
+        var dy = abs(pout.y - pin.y),
+            dx = abs(pout.x - pin.x);
+
+        var pathData = [];
+
+        pathData.push('M', pout.x, pout.y);
+        pathData.push('h', dx - dy);
+        pathData.push('L', pin.x, pin.y);
+
+        connection.setMarker(null);
+        connection.setPathData(pathData);
+    });
+});

+ 34 - 0
kityminder-editor/src/connect/l.js

@@ -0,0 +1,34 @@
+/**
+ * @fileOverview
+ *
+ * "L" 连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    connect.register('l', function(node, parent, connection) {
+
+        var po = parent.getLayoutVertexOut();
+        var pi = node.getLayoutVertexIn();
+        var vo = parent.getLayoutVectorOut();
+
+        var pathData = [];
+        var r = Math.round,
+            abs = Math.abs;
+
+        pathData.push('M', po.round());
+        if (abs(vo.x) > abs(vo.y)) {
+            pathData.push('H', r(pi.x));
+        } else {
+            pathData.push('V', pi.y);
+        }
+        pathData.push('L', pi);
+
+        connection.setPathData(pathData);
+    });
+});

+ 63 - 0
kityminder-editor/src/connect/poly.js

@@ -0,0 +1,63 @@
+/**
+ * @fileOverview
+ *
+ * 提供折线相连的方法
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    connect.register('poly', function(node, parent, connection, width) {
+
+        // 连线起点和终点
+        var po = parent.getLayoutVertexOut(),
+            pi = node.getLayoutVertexIn();
+
+        // 连线矢量和方向
+        var v = parent.getLayoutVectorOut().normalize();
+
+        var r = Math.round;
+        var abs = Math.abs;
+
+        var pathData = [];
+        pathData.push('M', r(po.x), r(po.y));
+
+        switch (true) {
+            case abs(v.x) > abs(v.y) && v.x < 0:
+                // left
+                pathData.push('h', -parent.getStyle('margin-left'));
+                pathData.push('v', pi.y - po.y);
+                pathData.push('H', pi.x);
+                break;
+
+            case abs(v.x) > abs(v.y) && v.x >= 0:
+                // right
+                pathData.push('h', parent.getStyle('margin-right'));
+                pathData.push('v', pi.y - po.y);
+                pathData.push('H', pi.x);
+                break;
+
+            case abs(v.x) <= abs(v.y) && v.y < 0:
+                // top
+                pathData.push('v', -parent.getStyle('margin-top'));
+                pathData.push('h', pi.x - po.x);
+                pathData.push('V', pi.y);
+                break;
+
+            case abs(v.x) <= abs(v.y) && v.y >= 0:
+                // bottom
+                pathData.push('v', parent.getStyle('margin-bottom'));
+                pathData.push('h', pi.x - po.x);
+                pathData.push('V', pi.y);
+                break;
+
+        }
+
+        connection.setMarker(null);
+        connection.setPathData(pathData);
+    });
+});

+ 50 - 0
kityminder-editor/src/connect/under.js

@@ -0,0 +1,50 @@
+/**
+ * @fileOverview
+ *
+ * 下划线连线
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var connect = require('../core/connect');
+
+    connect.register('under', function(node, parent, connection, width, color) {
+
+        var box = node.getLayoutBox(),
+            pBox = parent.getLayoutBox();
+
+        var start, end, vector;
+        var abs = Math.abs;
+        var pathData = [];
+        var side = box.x > pBox.x ? 'right' : 'left';
+
+
+        var radius = node.getStyle('connect-radius');
+        var underY = box.bottom + 3;
+        var startY = parent.getType() == 'sub' ? pBox.bottom + 3 : pBox.cy;
+        var p1, p2, p3, mx;
+
+        if (side == 'right') {
+            p1 = new kity.Point(pBox.right, startY);
+            p2 = new kity.Point(box.left - 10, underY);
+            p3 = new kity.Point(box.right, underY);
+        } else {
+            p1 = new kity.Point(pBox.left, startY);
+            p2 = new kity.Point(box.right + 10, underY);
+            p3 = new kity.Point(box.left, underY);
+        }
+
+        mx = (p1.x + p2.x) / 2;
+
+        pathData.push('M', p1);
+        pathData.push('C', mx, p1.y, mx, p2.y, p2);
+        pathData.push('L', p3);
+
+        connection.setMarker(null);
+
+        connection.setPathData(pathData);
+    });
+});

+ 34 - 0
kityminder-editor/src/core/_boxv.js

@@ -0,0 +1,34 @@
+/**
+ * @fileOverview
+ *
+ * 调试工具:为 kity.Box 提供一个可视化的渲染
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var Minder = require('./minder');
+
+    if (location.href.indexOf('boxv') != -1) {
+
+        var vrect;
+
+        Object.defineProperty(kity.Box.prototype, 'visualization', {
+            get: function() {
+                if (!vrect) return null;
+                return vrect.setBox(this);
+            }
+        });
+
+        Minder.registerInitHook(function() {
+            this.on('paperrender', function() {
+                vrect = new kity.Rect();
+                vrect.fill('rgba(200, 200, 200, .5)');
+                vrect.stroke('orange');
+                this.getRenderContainer().addShape(vrect);
+            });
+        });
+    }
+});

+ 43 - 0
kityminder-editor/src/core/animate.js

@@ -0,0 +1,43 @@
+/**
+ * @fileOverview
+ *
+ * 动画控制
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var Minder = require('./minder');
+
+    var animateDefaultOptions = {
+        enableAnimation: true,
+        layoutAnimationDuration: 300,
+        viewAnimationDuration: 100,
+        zoomAnimationDuration: 300
+    };
+    var resoredAnimationOptions = {};
+
+    Minder.registerInitHook(function() {
+        this.setDefaultOptions(animateDefaultOptions);
+        if (!this.getOption('enableAnimation')) {
+            this.disableAnimation();
+        }
+    });
+
+    Minder.prototype.enableAnimation = function() {
+        for (var name in animateDefaultOptions) {
+            if (animateDefaultOptions.hasOwnProperty(name)) {
+                this.setOption(resoredAnimationOptions[name]);
+            }
+        }
+    };
+
+    Minder.prototype.disableAnimation = function() {
+        for (var name in animateDefaultOptions) {
+            if (animateDefaultOptions.hasOwnProperty(name)) {
+                resoredAnimationOptions[name] = this.getOption(name);
+                this.setOption(name, 0);
+            }
+        }
+    };
+});

+ 169 - 0
kityminder-editor/src/core/command.js

@@ -0,0 +1,169 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+    var MinderEvent = require('./event');
+
+    var COMMAND_STATE_NORMAL = 0;
+    var COMMAND_STATE_DISABLED = -1;
+    var COMMAND_STATE_ACTIVED = 1;
+
+    /**
+     * 表示一个命令,包含命令的查询及执行
+     */
+    var Command = kity.createClass('Command', {
+        constructor: function() {
+            this._isContentChange = true;
+            this._isSelectionChange = false;
+        },
+
+        execute: function(minder, args) {
+            throw new Error('Not Implement: Command.execute()');
+        },
+
+        setContentChanged: function(val) {
+            this._isContentChange = !!val;
+        },
+
+        isContentChanged: function() {
+            return this._isContentChange;
+        },
+
+        setSelectionChanged: function(val) {
+            this._isSelectionChange = !!val;
+        },
+
+        isSelectionChanged: function() {
+            return this._isContentChange;
+        },
+
+        queryState: function(km) {
+            return COMMAND_STATE_NORMAL;
+        },
+
+        queryValue: function(km) {
+            return 0;
+        },
+
+        isNeedUndo: function() {
+            return true;
+        }
+    });
+
+    Command.STATE_NORMAL = COMMAND_STATE_NORMAL;
+    Command.STATE_ACTIVE = COMMAND_STATE_ACTIVED;
+    Command.STATE_DISABLED = COMMAND_STATE_DISABLED;
+
+    kity.extendClass(Minder, {
+        _getCommand: function(name) {
+            return this._commands[name.toLowerCase()];
+        },
+
+        _queryCommand: function(name, type, args) {
+            var cmd = this._getCommand(name);
+            if (cmd) {
+                var queryCmd = cmd['query' + type];
+                if (queryCmd)
+                    return queryCmd.apply(cmd, [this].concat(args));
+            }
+            return 0;
+        },
+
+        /**
+         * @method queryCommandState()
+         * @for Minder
+         * @description 查询指定命令的状态
+         *
+         * @grammar queryCommandName(name) => {number}
+         *
+         * @param {string} name 要查询的命令名称
+         *
+         * @return {number}
+         *   -1: 命令不存在或命令当前不可用
+         *    0: 命令可用
+         *    1: 命令当前可用并且已经执行过
+         */
+        queryCommandState: function(name) {
+            return this._queryCommand(name, 'State', [].slice.call(arguments, 1));
+        },
+
+        /**
+         * @method queryCommandValue()
+         * @for Minder
+         * @description 查询指定命令当前的执行值
+         *
+         * @grammar queryCommandValue(name) => {any}
+         *
+         * @param {string} name 要查询的命令名称
+         *
+         * @return {any}
+         *    如果命令不存在,返回 undefined
+         *    不同命令具有不同返回值,具体请查看 [Command](command) 章节
+         */
+        queryCommandValue: function(name) {
+            return this._queryCommand(name, 'Value', [].slice.call(arguments, 1));
+        },
+
+        /**
+         * @method execCommand()
+         * @for Minder
+         * @description 执行指定的命令。
+         *
+         * @grammar execCommand(name, args...)
+         *
+         * @param {string} name 要执行的命令名称
+         * @param {argument} args 要传递给命令的其它参数
+         */
+        execCommand: function(name) {
+            if (!name) return null;
+
+            name = name.toLowerCase();
+
+            var cmdArgs = [].slice.call(arguments, 1),
+                cmd, stoped, result, eventParams;
+            var me = this;
+            cmd = this._getCommand(name);
+
+            eventParams = {
+                command: cmd,
+                commandName: name.toLowerCase(),
+                commandArgs: cmdArgs
+            };
+            if (!cmd || !~this.queryCommandState(name)) {
+                return false;
+            }
+
+            if (!this._hasEnterExecCommand) {
+                this._hasEnterExecCommand = true;
+
+                stoped = this._fire(new MinderEvent('beforeExecCommand', eventParams, true));
+
+                if (!stoped) {
+                    this._fire(new MinderEvent('preExecCommand', eventParams, false));
+
+                    result = cmd.execute.apply(cmd, [me].concat(cmdArgs));
+
+                    this._fire(new MinderEvent('execCommand', eventParams, false));
+
+                    if (cmd.isContentChanged()) {
+                        this._firePharse(new MinderEvent('contentchange'));
+                    }
+
+                    this._interactChange();
+                }
+                this._hasEnterExecCommand = false;
+            } else {
+                result = cmd.execute.apply(cmd, [me].concat(cmdArgs));
+
+                if (!this._hasEnterExecCommand) {
+                    this._interactChange();
+                }
+            }
+
+            return result === undefined ? null : result;
+        }
+    });
+
+    module.exports = Command;
+});

+ 92 - 0
kityminder-editor/src/core/compatibility.js

@@ -0,0 +1,92 @@
+define(function(require, exports, module) {
+    var utils = require('./utils');
+
+    function compatibility(json) {
+
+        var version = json.version || (json.root ? '1.4.0' : '1.1.3');
+
+        switch (version) {
+            case '1.1.3':
+                c_113_120(json);
+                /* falls through */
+            case '1.2.0':
+            case '1.2.1':
+                c_120_130(json);
+                /* falls through */
+            case '1.3.0':
+            case '1.3.1':
+            case '1.3.2':
+            case '1.3.3':
+            case '1.3.4':
+            case '1.3.5':
+                /* falls through */
+                c_130_140(json);
+        }
+        return json;
+    }
+
+    function traverse(node, fn) {
+        fn(node);
+        if (node.children) node.children.forEach(function(child) {
+            traverse(child, fn);
+        });
+    }
+
+    /* 脑图数据升级 */
+    function c_120_130(json) {
+        traverse(json, function(node) {
+            var data = node.data;
+            delete data.layout_bottom_offset;
+            delete data.layout_default_offset;
+            delete data.layout_filetree_offset;
+        });
+    }
+
+    /**
+     * 脑图数据升级
+     * v1.1.3 => v1.2.0
+     * */
+    function c_113_120(json) {
+        // 原本的布局风格
+        var ocs = json.data.currentstyle;
+        delete json.data.currentstyle;
+
+        // 为 1.2 选择模板,同时保留老版本文件的皮肤
+        if (ocs == 'bottom') {
+            json.template = 'structure';
+            json.theme = 'snow';
+        } else if (ocs == 'default') {
+            json.template = 'default';
+            json.theme = 'classic';
+        }
+
+        traverse(json, function(node) {
+            var data = node.data;
+
+            // 升级优先级、进度图标
+            if ('PriorityIcon' in data) {
+                data.priority = data.PriorityIcon;
+                delete data.PriorityIcon;
+            }
+            if ('ProgressIcon' in data) {
+                data.progress = 1 + ((data.ProgressIcon - 1) << 1);
+                delete data.ProgressIcon;
+            }
+
+            // 删除过时属性
+            delete data.point;
+            delete data.layout;
+        });
+    }
+
+    function c_130_140(json) {
+        json.root = {
+            data: json.data,
+            children: json.children
+        };
+        delete json.data;
+        delete json.children;
+    }
+
+    return compatibility;
+});

+ 126 - 0
kityminder-editor/src/core/connect.js

@@ -0,0 +1,126 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Module = require('./module');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+
+    // 连线提供方
+    var _connectProviders = {};
+
+    function register(name, provider) {
+        _connectProviders[name] = provider;
+    }
+
+    register('default', function(node, parent, connection) {
+        connection.setPathData([
+            'M', parent.getLayoutVertexOut(),
+            'L', node.getLayoutVertexIn()
+        ]);
+    });
+
+    kity.extendClass(MinderNode, {
+        /**
+         * @private
+         * @method getConnect()
+         * @for MinderNode
+         * @description 获取当前节点的连线类型
+         *
+         * @grammar getConnect() => {string}
+         */
+        getConnect: function() {
+            return this.data.connect || 'default';
+        },
+
+        getConnectProvider: function() {
+            return _connectProviders[this.getConnect()] || _connectProviders['default'];
+        },
+
+        /**
+         * @private
+         * @method getConnection()
+         * @for MinderNode
+         * @description 获取当前节点的连线对象
+         *
+         * @grammar getConnection() => {kity.Path}
+         */
+        getConnection: function() {
+            return this._connection || null;
+        }
+    });
+
+    kity.extendClass(Minder, {
+
+        getConnectContainer: function() {
+            return this._connectContainer;
+        },
+
+        createConnect: function(node) {
+            if (node.isRoot()) return;
+
+            var connection = new kity.Path();
+
+            node._connection = connection;
+
+            this._connectContainer.addShape(connection);
+            this.updateConnect(node);
+        },
+
+        removeConnect: function(node) {
+            var me = this;
+            node.traverse(function(node) {
+                me._connectContainer.removeShape(node._connection);
+                node._connection = null;
+            });
+        },
+
+        updateConnect: function(node) {
+
+            var connection = node._connection;
+            var parent = node.parent;
+
+            if (!parent || !connection) return;
+
+            if (parent.isCollapsed()) {
+                connection.setVisible(false);
+                return;
+            }
+            connection.setVisible(true);
+
+            var provider = node.getConnectProvider();
+
+            var strokeColor = node.getStyle('connect-color') || 'white',
+                strokeWidth = node.getStyle('connect-width') || 2;
+
+            connection.stroke(strokeColor, strokeWidth);
+
+            provider(node, parent, connection, strokeWidth, strokeColor);
+
+            if (strokeWidth % 2 === 0) {
+                connection.setTranslate(0.5, 0.5);
+            } else {
+                connection.setTranslate(0, 0);
+            }
+        }
+    });
+
+    Module.register('Connect', {
+        init: function() {
+            this._connectContainer = new kity.Group().setId(utils.uuid('minder_connect_group'));
+            this.getRenderContainer().prependShape(this._connectContainer);
+        },
+        events: {
+            'nodeattach': function(e) {
+                this.createConnect(e.node);
+            },
+            'nodedetach': function(e) {
+                this.removeConnect(e.node);
+            },
+            'layoutapply layoutfinish noderender': function(e) {
+                this.updateConnect(e.node);
+            }
+        }
+    });
+
+    exports.register = register;
+});

+ 362 - 0
kityminder-editor/src/core/data.js

@@ -0,0 +1,362 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+    var MinderEvent = require('./event');
+    var compatibility = require('./compatibility');
+    var Promise = require('./promise');
+
+    var protocols = {};
+
+    function registerProtocol(name, protocol) {
+        protocols[name] = protocol;
+
+        for (var pname in protocols) {
+            if (protocols.hasOwnProperty(pname)) {
+                protocols[pname] = protocols[pname];
+                protocols[pname].name = pname;
+            }
+        }
+    }
+
+    function getRegisterProtocol(name) {
+        return name === undefined ? protocols : (protocols[name] || null);
+    }
+
+    exports.registerProtocol = registerProtocol;
+    exports.getRegisterProtocol = getRegisterProtocol;
+
+    // 导入导出
+    kity.extendClass(Minder, {
+
+        // 自动导入
+        setup: function(target) {
+            if (typeof target == 'string') {
+                target = document.querySelector(target);
+            }
+            if (!target) return;
+            var protocol = target.getAttribute('minder-data-type');
+            if (protocol in protocols) {
+                var data = target.textContent;
+                target.textContent = null;
+                this.renderTo(target);
+                this.importData(protocol, data);
+            }
+            return this;
+        },
+
+        /**
+         * @method exportJson()
+         * @for Minder
+         * @description
+         *     导出当前脑图数据为 JSON 对象,导出的数据格式请参考 [Data](data) 章节。
+         * @grammar exportJson() => {plain}
+         */
+        exportJson: function() {
+            /* 导出 node 上整棵树的数据为 JSON */
+            function exportNode(node) {
+                var exported = {};
+                exported.data = node.getData();
+                var childNodes = node.getChildren();
+                exported.children = [];
+                for (var i = 0; i < childNodes.length; i++) {
+                    exported.children.push(exportNode(childNodes[i]));
+                }
+                return exported;
+            }
+
+            var json = {
+                root: exportNode(this.getRoot())
+            };
+
+            json.template = this.getTemplate();
+            json.theme = this.getTheme();
+            json.version = Minder.version;
+
+            return JSON.parse(JSON.stringify(json));
+        },
+
+        /**
+         * function Text2Children(MinderNode, String) 
+         * @param {MinderNode} node 要导入数据的节点
+         * @param {String} text 导入的text数据
+         * @Desc: 用于批量插入子节点,并不会修改被插入的父节点
+         * @Editor: Naixor
+         * @Date: 2015.9.21
+         * @example: 用于批量导入如下类型的节点
+         *      234
+         *      3456346 asadf
+         *          12312414
+         *              wereww
+         *          12314
+         *      1231412
+         *      13123    
+         */
+        Text2Children: function (node, text) {
+            if (!(node instanceof kityminder.Node)) {
+                return;
+                // throw new Error('Json2Children::node is not a kityminder.Node type!');
+            };
+            var children = [],
+                jsonMap = {},
+                level = 0;
+
+            var LINE_SPLITTER = /\r|\n|\r\n/,
+                TAB_REGEXP = /^(\t|\x20{4})/;
+
+            var lines = text.split(LINE_SPLITTER),
+                line = '', jsonNode, i = 0;
+            var minder = this;
+
+            function isEmpty(line) {
+                return line === "" && !/\S/.test(line);
+            }
+
+            function getNode(line) {
+                return {
+                    data: {
+                        text: line.replace(/^(\t|\x20{4})+/, '').replace(/(\t|\x20{4})+$/, '')
+                    },
+                    children: []
+                }
+            }
+
+            function getLevel(text) {
+                var level = 0;
+                while (TAB_REGEXP.test(text)) {
+                    text = text.replace(TAB_REGEXP, '');
+                    level++;
+                }
+                return level;
+            }
+
+            function addChild(parent, node) {
+                parent.children.push(node);
+            }
+
+            function importChildren(node, children) {
+                for (var i = 0, l = children.length; i < l; i++) {
+                    var childNode = minder.createNode(null, node);
+                    childNode.setData('text', children[i].data.text || '');
+                    importChildren(childNode, children[i].children);
+                }
+            }
+
+            while ((line = lines[i++]) !== undefined) {
+                line = line.replace(/&nbsp;/g, '');
+                if (isEmpty(line)) continue;
+
+                level = getLevel(line);
+                jsonNode = getNode(line);
+                if (level === 0) {
+                    jsonMap = {};
+                    children.push(jsonNode);
+                    jsonMap[0] = children[children.length-1];
+                } else {
+                    if (!jsonMap[level-1]) {
+                        throw new Error('Invalid local format');
+                    };
+                    addChild(jsonMap[level-1], jsonNode);
+                    jsonMap[level] = jsonNode;
+                }
+            }
+
+            importChildren(node, children);
+            minder.refresh();
+        },
+
+        /**
+         * @method exportNode(MinderNode)
+         * @param  {MinderNode} node 当前要被导出的节点
+         * @return {Object}      返回只含有data和children的Object
+         * @Editor: Naixor
+         * @Date: 2015.9.22
+         */
+        exportNode: function (node) {
+            var exported = {};
+            exported.data = node.getData();
+            var childNodes = node.getChildren();
+            exported.children = [];
+            for (var i = 0; i < childNodes.length; i++) {
+                exported.children.push(this.exportNode(childNodes[i]));
+            }
+            return exported;
+        },
+        /**
+         * @method importNode()
+         * @description 根据纯json {data, children}数据转换成为脑图节点
+         * @Editor: Naixor
+         * @Date: 2015.9.20
+         */
+        importNode: function(node, json) {
+            var data = json.data;
+            node.data = {};
+
+            for (var field in data) {
+                node.setData(field, data[field]);
+            }
+
+            var childrenTreeData = json.children || [];
+            for (var i = 0; i < childrenTreeData.length; i++) {
+                var childNode = this.createNode(null, node);
+                this.importNode(childNode, childrenTreeData[i]);
+            }
+            return node;
+        },
+
+        /**
+         * @method importJson()
+         * @for Minder
+         * @description 导入脑图数据,数据为 JSON 对象,具体的数据字段形式请参考 [Data](data) 章节。
+         *
+         * @grammar importJson(json) => {this}
+         *
+         * @param {plain} json 要导入的数据
+         */
+        importJson: function(json) {
+            if (!json) return;
+
+            /**
+             * @event preimport
+             * @for Minder
+             * @when 导入数据之前
+             */
+            this._fire(new MinderEvent('preimport', null, false));
+
+            // 删除当前所有节点
+            while (this._root.getChildren().length) {
+                this.removeNode(this._root.getChildren()[0]);
+            }
+
+            json = compatibility(json);
+
+            this.importNode(this._root, json.root);
+
+            this.setTemplate(json.template || 'default');
+            this.setTheme(json.theme || null);
+            this.refresh();
+
+            /**
+             * @event import,contentchange,interactchange
+             * @for Minder
+             * @when 导入数据之后
+             */
+            this.fire('import');
+
+            this._firePharse({
+                type: 'contentchange'
+            });
+
+            this._interactChange();
+
+            return this;
+        },
+
+        /**
+         * @method exportData()
+         * @for Minder
+         * @description 使用指定使用的数据协议,导入脑图数据
+         *
+         * @grammar exportData(protocol) => Promise<data>
+         *
+         * @param {string} protocol 指定的数据协议(默认内置五种数据协议 `json`、`text`、`markdown`、`svg` 和 `png`)
+         */
+        exportData: function(protocolName, option) {
+            var json, protocol;
+
+            json = this.exportJson();
+
+            // 指定了协议进行导出,需要检测协议是否支持
+            if (protocolName) {
+                protocol = protocols[protocolName];
+
+                if (!protocol || !protocol.encode) {
+                    return Promise.reject(new Error('Not supported protocol:' + protocolName));
+                }
+            }
+
+            // 导出前抛个事件
+            this._fire(new MinderEvent('beforeexport', {
+                json: json,
+                protocolName: protocolName,
+                protocol: protocol
+            }));
+
+            return Promise.resolve(protocol.encode(json, this, option));
+        },
+
+        /**
+         * @method importData()
+         * @for Minder
+         * @description 使用指定的数据协议,导入脑图数据,覆盖当前实例的脑图
+         *
+         * @grammar importData(protocol, callback) => Promise<json>
+         *
+         * @param {string} protocol 指定的用于解析数据的数据协议(默认内置三种数据协议 `json`、`text` 和 `markdown` 的支持)
+         * @param {any} data 要导入的数据
+         */
+        importData: function(protocolName, data, option) {
+            var json, protocol;
+            var minder = this;
+
+            // 指定了协议进行导入,需要检测协议是否支持
+            if (protocolName) {
+                protocol = protocols[protocolName];
+
+                if (!protocol || !protocol.decode) {
+                    return Promise.reject(new Error('Not supported protocol:' + protocolName));
+                }
+            }
+
+            var params = {
+                local: data,
+                protocolName: protocolName,
+                protocol: protocol
+            };
+
+            // 导入前抛事件
+            this._fire(new MinderEvent('beforeimport', params));
+
+            return Promise.resolve(protocol.decode(data, this, option)).then(function(json) {
+                minder.importJson(json);
+                return json;
+            });
+        },
+
+        /**
+         * @method decodeData()
+         * @for Minder
+         * @description 使用指定的数据协议,解析为脑图数据,与 importData 的区别在于:不覆盖当前实例的脑图
+         *
+         * @grammar decodeData(protocol, callback) => Promise<json>
+         *
+         * @param {string} protocol 指定的用于解析数据的数据协议(默认内置三种数据协议 `json`、`text` 和 `markdown` 的支持)
+         * @param {any} data 要导入的数据
+         */
+        decodeData: function(protocolName, data, option) {
+            var json, protocol;
+            var minder = this;
+
+            // 指定了协议进行导入,需要检测协议是否支持
+            if (protocolName) {
+                protocol = protocols[protocolName];
+
+                if (!protocol || !protocol.decode) {
+                    return Promise.reject(new Error('Not supported protocol:' + protocolName));
+                }
+            }
+
+            var params = {
+                local: data,
+                protocolName: protocolName,
+                protocol: protocol
+            };
+
+            // 导入前抛事件
+            this._fire(new MinderEvent('beforeimport', params));
+
+            return Promise.resolve(protocol.decode(data, this, option))
+        }
+    });
+});

+ 269 - 0
kityminder-editor/src/core/event.js

@@ -0,0 +1,269 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+
+    /**
+     * @class MinderEvent
+     * @description 表示一个脑图中发生的事件
+     */
+    var MinderEvent = kity.createClass('MindEvent', {
+        constructor: function(type, params, canstop) {
+            params = params || {};
+            if (params.getType && params.getType() == 'ShapeEvent') {
+
+                /**
+                 * @property kityEvent
+                 * @for MinderEvent
+                 * @description 如果事件是从一个 kity 的事件派生的,会有 kityEvent 属性指向原来的 kity 事件
+                 * @type {KityEvent}
+                 */
+                this.kityEvent = params;
+
+                /**
+                 * @property originEvent
+                 * @for MinderEvent
+                 * @description 如果事件是从原声 Dom 事件派生的(如 click、mousemove 等),会有 originEvent 指向原来的 Dom 事件
+                 * @type {DomEvent}
+                 */
+                this.originEvent = params.originEvent;
+
+            } else if (params.target && params.preventDefault) {
+                this.originEvent = params;
+            } else {
+                kity.Utils.extend(this, params);
+            }
+
+            /**
+             * @property type
+             * @for MinderEvent
+             * @description 事件的类型,如 `click`、`contentchange` 等
+             * @type {string}
+             */
+            this.type = type;
+            this._canstop = canstop || false;
+        },
+
+        /**
+         * @method getPosition()
+         * @for MinderEvent
+         * @description 如果事件是从一个 kity 事件派生的,会有 `getPosition()` 获取事件发生的坐标
+         *
+         * @grammar getPosition(refer) => {kity.Point}
+         *
+         * @param {string|kity.Shape} refer
+         *     参照的坐标系,
+         *     `"screen"` - 以浏览器屏幕为参照坐标系
+         *     `"minder"` - (默认)以脑图画布为参照坐标系
+         *     `{kity.Shape}` - 指定以某个 kity 图形为参照坐标系
+         */
+        getPosition: function(refer) {
+            if (!this.kityEvent) return;
+            if (!refer || refer == 'minder') {
+                return this.kityEvent.getPosition(this.minder.getRenderContainer());
+            }
+            return this.kityEvent.getPosition.call(this.kityEvent, refer);
+        },
+
+        /**
+         * @method getTargetNode()
+         * @for MinderEvent
+         * @description 当发生的事件是鼠标事件时,获取事件位置命中的脑图节点
+         *
+         * @grammar getTargetNode() => {MinderNode}
+         */
+        getTargetNode: function() {
+            var findShape = this.kityEvent && this.kityEvent.targetShape;
+            if (!findShape) return null;
+            while (!findShape.minderNode && findShape.container) {
+                findShape = findShape.container;
+            }
+            var node = findShape.minderNode;
+            if (node && findShape.getOpacity() < 1) return null;
+            return node || null;
+        },
+
+        /**
+         * @method stopPropagation()
+         * @for MinderEvent
+         * @description 当发生的事件是鼠标事件时,获取事件位置命中的脑图节点
+         *
+         * @grammar getTargetNode() => {MinderNode}
+         */
+        stopPropagation: function() {
+            this._stoped = true;
+        },
+
+        stopPropagationImmediately: function() {
+            this._immediatelyStoped = true;
+            this._stoped = true;
+        },
+
+        shouldStopPropagation: function() {
+            return this._canstop && this._stoped;
+        },
+
+        shouldStopPropagationImmediately: function() {
+            return this._canstop && this._immediatelyStoped;
+        },
+        preventDefault: function() {
+            this.originEvent.preventDefault();
+        },
+        isRightMB: function() {
+            var isRightMB = false;
+            if (!this.originEvent) {
+                return false;
+            }
+            if ('which' in this.originEvent)
+                isRightMB = this.originEvent.which == 3;
+            else if ('button' in this.originEvent)
+                isRightMB = this.originEvent.button == 2;
+            return isRightMB;
+        },
+        getKeyCode: function() {
+            var evt = this.originEvent;
+            return evt.keyCode || evt.which;
+        }
+    });
+
+    Minder.registerInitHook(function(option) {
+        this._initEvents();
+    });
+
+    kity.extendClass(Minder, {
+
+        _initEvents: function() {
+            this._eventCallbacks = {};
+        },
+
+        _resetEvents: function() {
+            this._initEvents();
+            this._bindEvents();
+        },
+
+        _bindEvents: function() {
+            /* jscs:disable maximumLineLength */
+            this._paper.on('click dblclick mousedown contextmenu mouseup mousemove mouseover mousewheel DOMMouseScroll touchstart touchmove touchend dragenter dragleave drop', this._firePharse.bind(this));
+            if (window) {
+                window.addEventListener('resize', this._firePharse.bind(this));
+            }
+        },
+
+        /**
+         * @method dispatchKeyEvent
+         * @description 派发键盘(相关)事件到脑图实例上,让实例的模块处理
+         * @grammar dispatchKeyEvent(e) => {this}
+         * @param  {Event} e 原生的 Dom 事件对象
+         */
+        dispatchKeyEvent: function(e) {
+            this._firePharse(e);
+        },
+
+        _firePharse: function(e) {
+            var beforeEvent, preEvent, executeEvent;
+
+            if (e.type == 'DOMMouseScroll') {
+                e.type = 'mousewheel';
+                e.wheelDelta = e.originEvent.wheelDelta = e.originEvent.detail * -10;
+                e.wheelDeltaX = e.originEvent.mozMovementX;
+                e.wheelDeltaY = e.originEvent.mozMovementY;
+            }
+
+            beforeEvent = new MinderEvent('before' + e.type, e, true);
+            if (this._fire(beforeEvent)) {
+                return;
+            }
+            preEvent = new MinderEvent('pre' + e.type, e, true);
+            executeEvent = new MinderEvent(e.type, e, true);
+
+            if (this._fire(preEvent) ||
+                this._fire(executeEvent))
+                this._fire(new MinderEvent('after' + e.type, e, false));
+        },
+
+        _interactChange: function(e) {
+            var me = this;
+            if (me._interactScheduled) return;
+            setTimeout(function() {
+                me._fire(new MinderEvent('interactchange'));
+                me._interactScheduled = false;
+            }, 100);
+            me._interactScheduled = true;
+        },
+
+        _listen: function(type, callback) {
+            var callbacks = this._eventCallbacks[type] || (this._eventCallbacks[type] = []);
+            callbacks.push(callback);
+        },
+
+        _fire: function(e) {
+            /**
+             * @property minder
+             * @description 产生事件的 Minder 对象
+             * @for MinderShape
+             * @type {Minder}
+             */
+            e.minder = this;
+
+            var status = this.getStatus();
+            var callbacks = this._eventCallbacks[e.type.toLowerCase()] || [];
+
+            if (status) {
+                callbacks = callbacks.concat(this._eventCallbacks[status + '.' + e.type.toLowerCase()] || []);
+            }
+
+            if (callbacks.length === 0) {
+                return;
+            }
+
+            var lastStatus = this.getStatus();
+            for (var i = 0; i < callbacks.length; i++) {
+                callbacks[i].call(this, e);
+
+                /* this.getStatus() != lastStatus ||*/
+                if (e.shouldStopPropagationImmediately()) {
+                    break;
+                }
+            }
+
+            return e.shouldStopPropagation();
+        },
+
+        on: function(name, callback) {
+            var km = this;
+            name.split(/\s+/).forEach(function(n) {
+                km._listen(n.toLowerCase(), callback);
+            });
+            return this;
+        },
+
+        off: function(name, callback) {
+
+            var types = name.split(/\s+/);
+            var i, j, callbacks, removeIndex;
+            for (i = 0; i < types.length; i++) {
+
+                callbacks = this._eventCallbacks[types[i].toLowerCase()];
+                if (callbacks) {
+                    removeIndex = null;
+                    for (j = 0; j < callbacks.length; j++) {
+                        if (callbacks[j] == callback) {
+                            removeIndex = j;
+                        }
+                    }
+                    if (removeIndex !== null) {
+                        callbacks.splice(removeIndex, 1);
+                    }
+                }
+            }
+        },
+
+        fire: function(type, params) {
+            var e = new MinderEvent(type, params);
+            this._fire(e);
+            return this;
+        }
+    });
+
+    module.exports = MinderEvent;
+});

+ 41 - 0
kityminder-editor/src/core/focus.js

@@ -0,0 +1,41 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var Minder = require('./minder');
+
+    Minder.registerInitHook(function() {
+        this.on('beforemousedown', function(e) {
+            this.focus();
+            e.preventDefault();
+        });
+        this.on('paperrender', function() {
+            this.focus();
+        });
+    });
+
+    kity.extendClass(Minder, {
+        focus: function() {
+            if (!this.isFocused()) {
+                var renderTarget = this._renderTarget;
+                renderTarget.classList.add('focus');
+                this.renderNodeBatch(this.getSelectedNodes());
+            }
+            this.fire('focus');
+            return this;
+        },
+
+        blur: function() {
+            if (this.isFocused()) {
+                var renderTarget = this._renderTarget;
+                renderTarget.classList.remove('focus');
+                this.renderNodeBatch(this.getSelectedNodes());
+            }
+            this.fire('blur');
+            return this;
+        },
+
+        isFocused: function() {
+            var renderTarget = this._renderTarget;
+            return renderTarget && renderTarget.classList.contains('focus');
+        }
+    });
+});

+ 128 - 0
kityminder-editor/src/core/keymap.js

@@ -0,0 +1,128 @@
+define(function(require, exports, module) {
+    var keymap = {
+        'Backspace': 8,
+        'Tab': 9,
+        'Enter': 13,
+
+        'Shift': 16,
+        'Control': 17,
+        'Alt': 18,
+        'CapsLock': 20,
+
+        'Esc': 27,
+
+        'Spacebar': 32,
+
+        'PageUp': 33,
+        'PageDown': 34,
+        'End': 35,
+        'Home': 36,
+
+        'Insert': 45,
+
+        'Left': 37,
+        'Up': 38,
+        'Right': 39,
+        'Down': 40,
+
+        'direction': {
+            37: 1,
+            38: 1,
+            39: 1,
+            40: 1
+        },
+
+        'Del': 46,
+
+        'NumLock': 144,
+
+        'Cmd': 91,
+        'CmdFF': 224,
+        'F1': 112,
+        'F2': 113,
+        'F3': 114,
+        'F4': 115,
+        'F5': 116,
+        'F6': 117,
+        'F7': 118,
+        'F8': 119,
+        'F9': 120,
+        'F10': 121,
+        'F11': 122,
+        'F12': 123,
+
+        '`': 192,
+        '=': 187,
+        '-': 189,
+
+        '/': 191,
+        '.': 190,
+        controlKeys: {
+            16: 1,
+            17: 1,
+            18: 1,
+            20: 1,
+            91: 1,
+            224: 1
+        },
+        'notContentChange': {
+            13: 1,
+            9: 1,
+
+            33: 1,
+            34: 1,
+            35: 1,
+            36: 1,
+
+            16: 1,
+            17: 1,
+            18: 1,
+            20: 1,
+            91: 1,
+
+            //上下左右
+            37: 1,
+            38: 1,
+            39: 1,
+            40: 1,
+
+            113: 1,
+            114: 1,
+            115: 1,
+            144: 1,
+            27: 1
+        },
+
+        'isSelectedNodeKey': {
+            //上下左右
+            37: 1,
+            38: 1,
+            39: 1,
+            40: 1,
+            13: 1,
+            9: 1
+        }
+    };
+
+    // 小写适配
+    for (var key in keymap) {
+        if (keymap.hasOwnProperty(key)) {
+            keymap[key.toLowerCase()] = keymap[key];
+        }
+    }
+    var aKeyCode = 65;
+    var aCharCode = 'a'.charCodeAt(0);
+
+    // letters
+    'abcdefghijklmnopqrstuvwxyz'.split('').forEach(function(letter) {
+        keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode);
+    });
+
+    // numbers
+    var n = 9;
+    do {
+        keymap[n.toString()] = n + 48;
+    } while (--n);
+
+    module.exports = keymap;
+});

+ 66 - 0
kityminder-editor/src/core/keyreceiver.js

@@ -0,0 +1,66 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+
+    function listen(element, type, handler) {
+        type.split(' ').forEach(function(name) {
+            element.addEventListener(name, handler, false);
+        });
+    }
+
+    Minder.registerInitHook(function(option) {
+        this.setDefaultOptions({
+            enableKeyReceiver: true
+        });
+        if (this.getOption('enableKeyReceiver')) {
+            this.on('paperrender', function() {
+                this._initKeyReceiver();
+            });
+        }
+    });
+
+    kity.extendClass(Minder, {
+        _initKeyReceiver: function() {
+
+            if (this._keyReceiver) return;
+
+            var receiver = this._keyReceiver = document.createElement('input');
+            receiver.classList.add('km-receiver');
+
+            var renderTarget = this._renderTarget;
+            renderTarget.appendChild(receiver);
+
+            var minder = this;
+
+            listen(receiver, 'keydown keyup keypress copy paste blur focus input', function(e) {
+                switch (e.type) {
+                    case 'blur':
+                        minder.blur();
+                        break;
+                    case 'focus':
+                        minder.focus();
+                        break;
+                    case 'input':
+                        receiver.value = null;
+                        break;
+                }
+                minder._firePharse(e);
+                e.preventDefault();
+            });
+
+            this.on('focus', function() {
+                receiver.select();
+                receiver.focus();
+            });
+            this.on('blur', function() {
+                receiver.blur();
+            });
+
+            if (this.isFocused()) {
+                receiver.select();
+                receiver.focus();
+            }
+        }
+    });
+});

+ 11 - 0
kityminder-editor/src/core/kity.js

@@ -0,0 +1,11 @@
+/**
+ * @fileOverview
+ *
+ * Kity 引入
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    module.exports = window.kity;
+});

+ 523 - 0
kityminder-editor/src/core/layout.js

@@ -0,0 +1,523 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+    var MinderEvent = require('./event');
+    var Command = require('./command');
+
+    var _layouts = {};
+    var _defaultLayout;
+
+    function register(name, layout) {
+        _layouts[name] = layout;
+        _defaultLayout = _defaultLayout || name;
+    }
+
+    /**
+     * @class Layout 布局基类,具体布局需要从该类派生
+     */
+    var Layout = kity.createClass('Layout', {
+
+        /**
+         * @abstract
+         *
+         * 子类需要实现的布局算法,该算法输入一个节点,排布该节点的子节点(相对父节点的变换)
+         *
+         * @param  {MinderNode} node 需要布局的节点
+         *
+         * @example
+         *
+         * doLayout: function(node) {
+         *     var children = node.getChildren();
+         *     // layout calculation
+         *     children[i].setLayoutTransform(new kity.Matrix().translate(x, y));
+         * }
+         */
+        doLayout: function(parent, children) {
+            throw new Error('Not Implement: Layout.doLayout()');
+        },
+
+        /**
+         * 对齐指定的节点
+         *
+         * @param {Array<MinderNode>} nodes 要对齐的节点
+         * @param {string} border 对齐边界,允许取值 left, right, top, bottom
+         *
+         */
+        align: function(nodes, border, offset) {
+            var me = this;
+            offset = offset || 0;
+            nodes.forEach(function(node) {
+                var tbox = me.getTreeBox([node]);
+                var matrix = node.getLayoutTransform();
+                switch (border) {
+                    case 'left':
+                        return matrix.translate(offset - tbox.left, 0);
+                    case 'right':
+                        return matrix.translate(offset - tbox.right, 0);
+                    case 'top':
+                        return matrix.translate(0, offset - tbox.top);
+                    case 'bottom':
+                        return matrix.translate(0, offset - tbox.bottom);
+                }
+            });
+        },
+
+        stack: function(nodes, axis, distance) {
+            var me = this;
+
+            var position = 0;
+
+            distance = distance || function(node, next, axis) {
+                return node.getStyle({
+                    x: 'margin-right',
+                    y: 'margin-bottom'
+                }[axis]) + next.getStyle({
+                    x: 'margin-left',
+                    y: 'margin-top'
+                }[axis]);
+            };
+
+            nodes.forEach(function(node, index, nodes) {
+                var tbox = me.getTreeBox([node]);
+
+                var size = {
+                    x: tbox.width,
+                    y: tbox.height
+                }[axis];
+                var offset = {
+                    x: tbox.left,
+                    y: tbox.top
+                }[axis];
+
+                var matrix = node.getLayoutTransform();
+
+                if (axis == 'x') {
+                    matrix.translate(position - offset, 0);
+                } else {
+                    matrix.translate(0, position - offset);
+                }
+                position += size;
+                if (nodes[index + 1])
+                    position += distance(node, nodes[index + 1], axis);
+            });
+            return position;
+        },
+
+        move: function(nodes, dx, dy) {
+            nodes.forEach(function(node) {
+                node.getLayoutTransform().translate(dx, dy);
+            });
+        },
+
+        /**
+         * 工具方法:获取给点的节点所占的布局区域
+         *
+         * @param  {MinderNode[]} nodes 需要计算的节点
+         *
+         * @return {Box} 计算结果
+         */
+        getBranchBox: function(nodes) {
+            var box = new kity.Box();
+            var i, node, matrix, contentBox;
+            for (i = 0; i < nodes.length; i++) {
+                node = nodes[i];
+                matrix = node.getLayoutTransform();
+                contentBox = node.getContentBox();
+                box = box.merge(matrix.transformBox(contentBox));
+            }
+
+            return box;
+        },
+
+        /**
+         * 工具方法:计算给定的节点的子树所占的布局区域
+         *
+         * @param  {MinderNode} nodes 需要计算的节点
+         *
+         * @return {Box} 计算的结果
+         */
+        getTreeBox: function(nodes) {
+
+            var i, node, matrix, treeBox;
+
+            var box = new kity.Box();
+
+            if (!(nodes instanceof Array)) nodes = [nodes];
+
+            for (i = 0; i < nodes.length; i++) {
+                node = nodes[i];
+                matrix = node.getLayoutTransform();
+
+                treeBox = node.getContentBox();
+
+                if (node.isExpanded() && node.children.length) {
+                    treeBox = treeBox.merge(this.getTreeBox(node.children));
+                }
+
+                box = box.merge(matrix.transformBox(treeBox));
+            }
+
+            return box;
+        },
+
+        getOrderHint: function(node) {
+            return [];
+        }
+    });
+
+    Layout.register = register;
+
+    Minder.registerInitHook(function(options) {
+        this.refresh();
+    });
+
+    /**
+     * 布局支持池子管理
+     */
+    utils.extend(Minder, {
+
+        getLayoutList: function() {
+            return _layouts;
+        },
+
+        getLayoutInstance: function(name) {
+            var LayoutClass = _layouts[name];
+            if (!LayoutClass) throw new Error('Missing Layout: ' + name);
+            var layout = new LayoutClass();
+            return layout;
+        }
+    });
+
+    /**
+     * MinderNode 上的布局支持
+     */
+    kity.extendClass(MinderNode, {
+
+        /**
+         * 获得当前节点的布局名称
+         *
+         * @return {String}
+         */
+        getLayout: function() {
+            var layout = this.getData('layout');
+
+            layout = layout || (this.isRoot() ? _defaultLayout : this.parent.getLayout());
+
+            return layout;
+        },
+
+        setLayout: function(name) {
+            if (name) {
+                if (name == 'inherit') {
+                    this.setData('layout');
+                } else {
+                    this.setData('layout', name);
+                }
+            }
+            return this;
+        },
+
+        layout: function(name) {
+
+            this.setLayout(name).getMinder().layout();
+
+            return this;
+        },
+
+        getLayoutInstance: function() {
+            return Minder.getLayoutInstance(this.getLayout());
+        },
+
+        getOrderHint: function(refer) {
+            return this.parent.getLayoutInstance().getOrderHint(this);
+        },
+
+        /**
+         * 获取当前节点相对于父节点的布局变换
+         */
+        getLayoutTransform: function() {
+            return this._layoutTransform || new kity.Matrix();
+        },
+
+        /**
+         * 第一轮布局计算后,获得的全局布局位置
+         *
+         * @return {[type]} [description]
+         */
+        getGlobalLayoutTransformPreview: function() {
+            var pMatrix = this.parent ? this.parent.getLayoutTransform() : new kity.Matrix();
+            var matrix = this.getLayoutTransform();
+            var offset = this.getLayoutOffset();
+            if (offset) {
+                matrix = matrix.clone().translate(offset.x, offset.y);
+            }
+            return pMatrix.merge(matrix);
+        },
+
+        getLayoutPointPreview: function() {
+            return this.getGlobalLayoutTransformPreview().transformPoint(new kity.Point());
+        },
+
+        /**
+         * 获取节点相对于全局的布局变换
+         */
+        getGlobalLayoutTransform: function() {
+            if (this._globalLayoutTransform) {
+                return this._globalLayoutTransform;
+            } else if (this.parent) {
+                return this.parent.getGlobalLayoutTransform();
+            } else {
+                return new kity.Matrix();
+            }
+        },
+
+        /**
+         * 设置当前节点相对于父节点的布局变换
+         */
+        setLayoutTransform: function(matrix) {
+            this._layoutTransform = matrix;
+            return this;
+        },
+
+        /**
+         * 设置当前节点相对于全局的布局变换(冗余优化)
+         */
+        setGlobalLayoutTransform: function(matrix) {
+            this.getRenderContainer().setMatrix(this._globalLayoutTransform = matrix);
+            return this;
+        },
+
+        setVertexIn: function(p) {
+            this._vertexIn = p;
+        },
+
+        setVertexOut: function(p) {
+            this._vertexOut = p;
+        },
+
+        getVertexIn: function() {
+            return this._vertexIn || new kity.Point();
+        },
+
+        getVertexOut: function() {
+            return this._vertexOut || new kity.Point();
+        },
+
+        getLayoutVertexIn: function() {
+            return this.getGlobalLayoutTransform().transformPoint(this.getVertexIn());
+        },
+
+        getLayoutVertexOut: function() {
+            return this.getGlobalLayoutTransform().transformPoint(this.getVertexOut());
+        },
+
+        setLayoutVectorIn: function(v) {
+            this._layoutVectorIn = v;
+            return this;
+        },
+
+        setLayoutVectorOut: function(v) {
+            this._layoutVectorOut = v;
+            return this;
+        },
+
+        getLayoutVectorIn: function() {
+            return this._layoutVectorIn || new kity.Vector();
+        },
+
+        getLayoutVectorOut: function() {
+            return this._layoutVectorOut || new kity.Vector();
+        },
+
+        getLayoutBox: function() {
+            var matrix = this.getGlobalLayoutTransform();
+            return matrix.transformBox(this.getContentBox());
+        },
+
+        getLayoutPoint: function() {
+            var matrix = this.getGlobalLayoutTransform();
+            return matrix.transformPoint(new kity.Point());
+        },
+
+        getLayoutOffset: function() {
+            if (!this.parent) return new kity.Point();
+
+            // 影响当前节点位置的是父节点的布局
+            var data = this.getData('layout_' + this.parent.getLayout() + '_offset');
+
+            if (data) return new kity.Point(data.x, data.y);
+
+            return new kity.Point();
+        },
+
+        setLayoutOffset: function(p) {
+            if (!this.parent) return this;
+
+            this.setData('layout_' + this.parent.getLayout() + '_offset', p ? {
+                x: p.x,
+                y: p.y
+            } : undefined);
+
+            return this;
+        },
+
+        hasLayoutOffset: function() {
+            return !!this.getData('layout_' + this.parent.getLayout() + '_offset');
+        },
+
+        resetLayoutOffset: function() {
+            return this.setLayoutOffset(null);
+        },
+
+        getLayoutRoot: function() {
+            if (this.isLayoutRoot()) {
+                return this;
+            }
+            return this.parent.getLayoutRoot();
+        },
+
+        isLayoutRoot: function() {
+            return this.getData('layout') || this.isRoot();
+        }
+    });
+
+    /**
+     * Minder 上的布局支持
+     */
+    kity.extendClass(Minder, {
+
+        layout: function() {
+
+            var duration = this.getOption('layoutAnimationDuration');
+
+            this.getRoot().traverse(function(node) {
+                // clear last results
+                node.setLayoutTransform(null);
+            });
+
+            function layoutNode(node, round) {
+
+                // layout all children first
+                // 剪枝:收起的节点无需计算
+                if (node.isExpanded() || true) {
+                    node.children.forEach(function(child) {
+                        layoutNode(child, round);
+                    });
+                }
+
+                var layout = node.getLayoutInstance();
+                // var childrenInFlow = node.getChildren().filter(function(child) {
+                //     return !child.hasLayoutOffset();
+                // });
+                layout.doLayout(node, node.getChildren(), round);
+            }
+
+            // 第一轮布局
+            layoutNode(this.getRoot(), 1);
+
+            // 第二轮布局
+            layoutNode(this.getRoot(), 2);
+
+            var minder = this;
+            this.applyLayoutResult(this.getRoot(), duration, function() {
+                /**
+                 * 当节点>200, 不使用动画时, 此处逻辑变为同步逻辑, 外部minder.on事件无法
+                 * 被提前录入, 因此增加setTimeout
+                 * @author Naixor
+                 */
+                setTimeout(function () {
+                    minder.fire('layoutallfinish');
+                }, 0);
+            });
+
+            return this.fire('layout');
+        },
+
+        refresh: function() {
+            this.getRoot().renderTree();
+            this.layout().fire('contentchange')._interactChange();
+            return this;
+        },
+
+        applyLayoutResult: function(root, duration, callback) {
+            root = root || this.getRoot();
+            var me = this;
+
+            var complex = root.getComplex();
+
+            function consume() {
+                if (!--complex) {
+                    if (callback) {
+                        callback();
+                    }
+                }
+            }
+
+            // 节点复杂度大于 100,关闭动画
+            if (complex > 200) duration = 0;
+
+            function applyMatrix(node, matrix) {
+                node.setGlobalLayoutTransform(matrix);
+
+                me.fire('layoutapply', {
+                    node: node,
+                    matrix: matrix
+                });
+            }
+
+            function apply(node, pMatrix) {
+                var matrix = node.getLayoutTransform().merge(pMatrix.clone());
+                var lastMatrix = node.getGlobalLayoutTransform() || new kity.Matrix();
+
+                var offset = node.getLayoutOffset();
+                matrix.translate(offset.x, offset.y);
+
+                matrix.m.e = Math.round(matrix.m.e);
+                matrix.m.f = Math.round(matrix.m.f);
+
+                // 如果当前有动画,停止动画
+                if (node._layoutTimeline) {
+                    node._layoutTimeline.stop();
+                    node._layoutTimeline = null;
+                }
+
+                // 如果要求以动画形式来更新,创建动画
+                if (duration) {
+                    node._layoutTimeline = new kity.Animator(lastMatrix, matrix, applyMatrix)
+                        .start(node, duration, 'ease')
+                        .on('finish', function() {
+                            //可能性能低的时候会丢帧,手动添加一帧
+                            setTimeout(function() {
+                                applyMatrix(node, matrix);
+                                me.fire('layoutfinish', {
+                                    node: node,
+                                    matrix: matrix
+                                });
+                                consume();
+                            }, 150);
+                        });
+                }
+
+                // 否则直接更新
+                else {
+                    applyMatrix(node, matrix);
+                    me.fire('layoutfinish', {
+                        node: node,
+                        matrix: matrix
+                    });
+                    consume();
+                }
+
+                for (var i = 0; i < node.children.length; i++) {
+                    apply(node.children[i], matrix);
+                }
+            }
+            apply(root, root.parent ? root.parent.getGlobalLayoutTransform() : new kity.Matrix());
+            return this;
+        }
+    });
+
+    module.exports = Layout;
+});

+ 40 - 0
kityminder-editor/src/core/minder.js

@@ -0,0 +1,40 @@
+/**
+ * @fileOverview
+ *
+ * KityMinder 类,暴露在 window 上的唯一变量
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+
+    var _initHooks = [];
+
+    var Minder = kity.createClass('Minder', {
+        constructor: function(options) {
+            this._options = utils.extend({}, options);
+
+            var initHooks = _initHooks.slice();
+
+            var initHook;
+            while (initHooks.length) {
+                initHook = initHooks.shift();
+                if (typeof(initHook) == 'function') {
+                    initHook.call(this, this._options);
+                }
+            }
+
+            this.fire('finishInitHook');
+        }
+    });
+
+    Minder.version = '1.4.43';
+
+    Minder.registerInitHook = function(hook) {
+        _initHooks.push(hook);
+    };
+
+    module.exports = Minder;
+});

+ 151 - 0
kityminder-editor/src/core/module.js

@@ -0,0 +1,151 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+
+    /* 已注册的模块 */
+    var _modules = {};
+
+    exports.register = function(name, module) {
+        _modules[name] = module;
+    };
+
+    /* 模块初始化 */
+    Minder.registerInitHook(function() {
+        this._initModules();
+    });
+
+    // 模块声明周期维护
+    kity.extendClass(Minder, {
+        _initModules: function() {
+            var modulesPool = _modules;
+            var modulesToLoad = this._options.modules || utils.keys(modulesPool);
+
+            this._commands = {};
+            this._query = {};
+            this._modules = {};
+            this._rendererClasses = {};
+
+            var i, name, type, module, moduleDeals,
+                dealCommands, dealEvents, dealRenderers;
+
+            var me = this;
+            for (i = 0; i < modulesToLoad.length; i++) {
+                name = modulesToLoad[i];
+
+                if (!modulesPool[name]) continue;
+
+                // 执行模块初始化,抛出后续处理对象
+
+                if (typeof(modulesPool[name]) == 'function') {
+                    moduleDeals = modulesPool[name].call(me);
+                } else {
+                    moduleDeals = modulesPool[name];
+                }
+                this._modules[name] = moduleDeals;
+
+                if (!moduleDeals) continue;
+
+                if (moduleDeals.defaultOptions) {
+                    me.setDefaultOptions(moduleDeals.defaultOptions);
+                }
+
+                if (moduleDeals.init) {
+                    moduleDeals.init.call(me, this._options);
+                }
+
+                /**
+                 * @Desc: 判断是否支持原生clipboard事件,如果支持,则对pager添加其监听
+                 * @Editor: Naixor
+                 * @Date: 2015.9.20
+                 */
+                /**
+                 * 由于当前脑图解构问题,clipboard暂时全权交由玩不托管
+                 * @Editor: Naixor
+                 * @Date: 2015.9.24
+                 */
+                // if (name === 'ClipboardModule' && this.supportClipboardEvent  && !kity.Browser.gecko) {
+                //     var on = function () {
+                //         var clipBoardReceiver = this.clipBoardReceiver || document;
+                        
+                //         if (document.addEventListener) {
+                //             clipBoardReceiver.addEventListener.apply(this, arguments);
+                //         } else {
+                //             arguments[0] = 'on' + arguments[0];
+                //             clipBoardReceiver.attachEvent.apply(this, arguments);
+                //         }
+                //     }
+                //     for (var command in moduleDeals.clipBoardEvents) {
+                //         on(command, moduleDeals.clipBoardEvents[command]);
+                //     }
+                // };
+
+                // command加入命令池子
+                dealCommands = moduleDeals.commands;
+                for (name in dealCommands) {
+                    this._commands[name.toLowerCase()] = new dealCommands[name]();
+                }
+
+                // 绑定事件
+                dealEvents = moduleDeals.events;
+                if (dealEvents) {
+                    for (type in dealEvents) {
+                        me.on(type, dealEvents[type]);
+                    }
+                }
+
+                // 渲染器
+                dealRenderers = moduleDeals.renderers;
+
+                if (dealRenderers) {
+
+                    for (type in dealRenderers) {
+                        this._rendererClasses[type] = this._rendererClasses[type] || [];
+
+                        if (utils.isArray(dealRenderers[type])) {
+                            this._rendererClasses[type] = this._rendererClasses[type].concat(dealRenderers[type]);
+                        } else {
+                            this._rendererClasses[type].push(dealRenderers[type]);
+                        }
+                    }
+                }
+
+                //添加模块的快捷键
+                if (moduleDeals.commandShortcutKeys) {
+                    this.addCommandShortcutKeys(moduleDeals.commandShortcutKeys);
+                }
+            }
+        },
+
+        _garbage: function() {
+            this.clearSelect();
+
+            while (this._root.getChildren().length) {
+                this._root.removeChild(0);
+            }
+        },
+
+        destroy: function() {
+            var modules = this._modules;
+
+            this._resetEvents();
+            this._garbage();
+
+            for (var key in modules) {
+                if (!modules[key].destroy) continue;
+                modules[key].destroy.call(this);
+            }
+        },
+
+        reset: function() {
+            var modules = this._modules;
+
+            this._garbage();
+
+            for (var key in modules) {
+                if (!modules[key].reset) continue;
+                modules[key].reset.call(this);
+            }
+        }
+    });
+});

+ 407 - 0
kityminder-editor/src/core/node.js

@@ -0,0 +1,407 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+
+    /**
+     * @class MinderNode
+     *
+     * 表示一个脑图节点
+     */
+    var MinderNode = kity.createClass('MinderNode', {
+
+        /**
+         * 创建一个游离的脑图节点
+         *
+         * @param {String|Object} textOrData
+         *     节点的初始数据或文本
+         */
+        constructor: function(textOrData) {
+
+            // 指针
+            this.parent = null;
+            this.root = this;
+            this.children = [];
+
+            // 数据
+            this.data = {
+                id: utils.guid(),
+                created: +new Date()
+            };
+
+            // 绘图容器
+            this.initContainers();
+
+            if (utils.isString(textOrData)) {
+                this.setText(textOrData);
+            } else if (utils.isObject(textOrData)) {
+                utils.extend(this.data, textOrData);
+            }
+        },
+
+        initContainers: function() {
+            this.rc = new kity.Group().setId(utils.uuid('minder_node'));
+            this.rc.minderNode = this;
+        },
+
+        /**
+         * 判断节点是否根节点
+         */
+        isRoot: function() {
+            return this.root === this;
+        },
+
+        /**
+         * 判断节点是否叶子
+         */
+        isLeaf: function() {
+            return this.children.length === 0;
+        },
+
+        /**
+         * 获取节点的根节点
+         */
+        getRoot: function() {
+            return this.root || this;
+        },
+
+        /**
+         * 获得节点的父节点
+         */
+        getParent: function() {
+            return this.parent;
+        },
+
+        getSiblings: function() {
+            var children = this.parent.children;
+            var siblings = [];
+            var self = this;
+            children.forEach(function(child) {
+                if (child != self) siblings.push(child);
+            });
+            return siblings;
+        },
+
+        /**
+         * 获得节点的深度
+         */
+        getLevel: function() {
+            var level = 0,
+                ancestor = this.parent;
+            while (ancestor) {
+                level++;
+                ancestor = ancestor.parent;
+            }
+            return level;
+        },
+
+        /**
+         * 获得节点的复杂度(即子树中节点的数量)
+         */
+        getComplex: function() {
+            var complex = 0;
+            this.traverse(function() {
+                complex++;
+            });
+            return complex;
+        },
+
+        /**
+         * 获得节点的类型(root|main|sub)
+         */
+        getType: function(type) {
+            this.type = ['root', 'main', 'sub'][Math.min(this.getLevel(), 2)];
+            return this.type;
+        },
+
+        /**
+         * 判断当前节点是否被测试节点的祖先
+         * @param  {MinderNode}  test 被测试的节点
+         */
+        isAncestorOf: function(test) {
+            var ancestor = test.parent;
+            while (ancestor) {
+                if (ancestor == this) return true;
+                ancestor = ancestor.parent;
+            }
+            return false;
+        },
+
+        getData: function(key) {
+            return key ? this.data[key] : this.data;
+        },
+
+        setData: function(key, value) {
+            if (typeof key == 'object') {
+                var data = key;
+                for (key in data) if (data.hasOwnProperty(key)) {
+                    this.data[key] = data[key];
+                }
+            }
+            else {
+                this.data[key] = value;
+            }
+            return this;
+        },
+
+        /**
+         * 设置节点的文本数据
+         * @param {String} text 文本数据
+         */
+        setText: function(text) {
+            return this.data.text = text;
+        },
+
+        /**
+         * 获取节点的文本数据
+         * @return {String}
+         */
+        getText: function() {
+            return this.data.text || null;
+        },
+
+        /**
+         * 先序遍历当前节点树
+         * @param  {Function} fn 遍历函数
+         */
+        preTraverse: function(fn, excludeThis) {
+            var children = this.getChildren();
+            if (!excludeThis) fn(this);
+            for (var i = 0; i < children.length; i++) {
+                children[i].preTraverse(fn);
+            }
+        },
+
+        /**
+         * 后序遍历当前节点树
+         * @param  {Function} fn 遍历函数
+         */
+        postTraverse: function(fn, excludeThis) {
+            var children = this.getChildren();
+            for (var i = 0; i < children.length; i++) {
+                children[i].postTraverse(fn);
+            }
+            if (!excludeThis) fn(this);
+        },
+
+        traverse: function(fn, excludeThis) {
+            return this.postTraverse(fn, excludeThis);
+        },
+
+        getChildren: function() {
+            return this.children;
+        },
+
+        getIndex: function() {
+            return this.parent ? this.parent.children.indexOf(this) : -1;
+        },
+
+        insertChild: function(node, index) {
+            if (index === undefined) {
+                index = this.children.length;
+            }
+            if (node.parent) {
+                node.parent.removeChild(node);
+            }
+            node.parent = this;
+            node.root = this.root;
+
+            this.children.splice(index, 0, node);
+        },
+
+        appendChild: function(node) {
+            return this.insertChild(node);
+        },
+
+        prependChild: function(node) {
+            return this.insertChild(node, 0);
+        },
+
+        removeChild: function(elem) {
+            var index = elem,
+                removed;
+            if (elem instanceof MinderNode) {
+                index = this.children.indexOf(elem);
+            }
+            if (index >= 0) {
+                removed = this.children.splice(index, 1)[0];
+                removed.parent = null;
+                removed.root = removed;
+            }
+        },
+
+        clearChildren: function() {
+            this.children = [];
+        },
+
+        getChild: function(index) {
+            return this.children[index];
+        },
+
+        getRenderContainer: function() {
+            return this.rc;
+        },
+
+        getCommonAncestor: function(node) {
+            return MinderNode.getCommonAncestor(this, node);
+        },
+
+        contains: function(node) {
+            return this == node || this.isAncestorOf(node);
+        },
+
+        clone: function() {
+            var cloned = new MinderNode();
+
+            cloned.data = utils.clone(this.data);
+
+            this.children.forEach(function(child) {
+                cloned.appendChild(child.clone());
+            });
+
+            return cloned;
+        },
+
+        compareTo: function(node) {
+
+            if (!utils.comparePlainObject(this.data, node.data)) return false;
+            if (!utils.comparePlainObject(this.temp, node.temp)) return false;
+            if (this.children.length != node.children.length) return false;
+
+            var i = 0;
+            while (this.children[i]) {
+                if (!this.children[i].compareTo(node.children[i])) return false;
+                i++;
+            }
+
+            return true;
+        },
+
+        getMinder: function() {
+            return this.getRoot().minder;
+        }
+    });
+
+    MinderNode.getCommonAncestor = function(nodeA, nodeB) {
+        if (nodeA instanceof Array) {
+            return MinderNode.getCommonAncestor.apply(this, nodeA);
+        }
+        switch (arguments.length) {
+            case 1:
+                return nodeA.parent || nodeA;
+
+            case 2:
+                if (nodeA.isAncestorOf(nodeB)) {
+                    return nodeA;
+                }
+                if (nodeB.isAncestorOf(nodeA)) {
+                    return nodeB;
+                }
+                var ancestor = nodeA.parent;
+                while (ancestor && !ancestor.isAncestorOf(nodeB)) {
+                    ancestor = ancestor.parent;
+                }
+                return ancestor;
+
+            default:
+                return Array.prototype.reduce.call(arguments,
+                    function(prev, current) {
+                        return MinderNode.getCommonAncestor(prev, current);
+                    },
+                    nodeA
+                );
+        }
+    };
+
+    kity.extendClass(Minder, {
+
+        getRoot: function() {
+            return this._root;
+        },
+
+        setRoot: function(root) {
+            this._root = root;
+            root.minder = this;
+        },
+
+        getAllNode: function() {
+            var nodes = [];
+            this.getRoot().traverse(function(node) {
+                nodes.push(node);
+            });
+            return nodes;
+        },
+
+        getNodeById: function(id) {
+            return this.getNodesById([id])[0];
+        },
+
+        getNodesById: function(ids) {
+            var nodes = this.getAllNode();
+            var result = [];
+            nodes.forEach(function(node) {
+                if (ids.indexOf(node.getData('id')) != -1) {
+                    result.push(node);
+                }
+            });
+            return result;
+        },
+
+        createNode: function(textOrData, parent, index) {
+            var node = new MinderNode(textOrData);
+            this.fire('nodecreate', {
+                node: node,
+                parent: parent,
+                index: index
+            });
+            this.appendNode(node, parent, index);
+            return node;
+        },
+
+        appendNode: function(node, parent, index) {
+            if (parent) parent.insertChild(node, index);
+            this.attachNode(node);
+            return this;
+        },
+
+        removeNode: function(node) {
+            if (node.parent) {
+                node.parent.removeChild(node);
+                this.detachNode(node);
+                this.fire('noderemove', {
+                    node: node
+                });
+            }
+        },
+
+        attachNode: function(node) {
+            var rc = this.getRenderContainer();
+            node.traverse(function(current) {
+                current.attached = true;
+                rc.addShape(current.getRenderContainer());
+            });
+            rc.addShape(node.getRenderContainer());
+            this.fire('nodeattach', {
+                node: node
+            });
+        },
+
+        detachNode: function(node) {
+            var rc = this.getRenderContainer();
+            node.traverse(function(current) {
+                current.attached = false;
+                rc.removeShape(current.getRenderContainer());
+            });
+            this.fire('nodedetach', {
+                node: node
+            });
+        },
+
+        getMinderTitle: function() {
+            return this.getRoot().getText();
+        }
+
+    });
+
+    module.exports = MinderNode;
+});

+ 34 - 0
kityminder-editor/src/core/option.js

@@ -0,0 +1,34 @@
+/**
+ * @fileOverview
+ *
+ * 提供脑图选项支持
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+
+    Minder.registerInitHook(function(options) {
+        this._defaultOptions = {};
+    });
+
+    kity.extendClass(Minder, {
+        setDefaultOptions: function(options) {
+            utils.extend(this._defaultOptions, options);
+            return this;
+        },
+        getOption: function(key) {
+            if (key) {
+                return key in this._options ? this._options[key] : this._defaultOptions[key];
+            } else {
+                return utils.extend({}, this._defaultOptions, this._options);
+            }
+        },
+        setOption: function(key, value) {
+            this._options[key] = value;
+        }
+    });
+});

+ 76 - 0
kityminder-editor/src/core/paper.js

@@ -0,0 +1,76 @@
+/**
+ * @fileOverview
+ *
+ * 初始化渲染容器
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+
+    Minder.registerInitHook(function() {
+        this._initPaper();
+    });
+
+    kity.extendClass(Minder, {
+
+        _initPaper: function() {
+
+            this._paper = new kity.Paper();
+            this._paper._minder = this;
+            this._paper.getNode().ondragstart = function(e) {
+                e.preventDefault();
+            };
+            this._paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)');
+
+            this._addRenderContainer();
+
+            this.setRoot(this.createNode());
+
+            if (this._options.renderTo) {
+                this.renderTo(this._options.renderTo);
+            }
+        },
+
+        _addRenderContainer: function() {
+            this._rc = new kity.Group().setId(utils.uuid('minder'));
+            this._paper.addShape(this._rc);
+        },
+
+        renderTo: function(target) {
+            if (typeof(target) == 'string') {
+                target = document.querySelector(target);
+            }
+            if (target) {
+                if (target.tagName.toLowerCase() == 'script') {
+                    var newTarget = document.createElement('div');
+                    newTarget.id = target.id;
+                    newTarget.class = target.class;
+                    target.parentNode.insertBefore(newTarget, target);
+                    target.parentNode.removeChild(target);
+                    target = newTarget;
+                }
+                target.classList.add('km-view');
+                this._paper.renderTo(this._renderTarget = target);
+                this._bindEvents();
+                this.fire('paperrender');
+            }
+            return this;
+        },
+
+        getRenderContainer: function() {
+            return this._rc;
+        },
+
+        getPaper: function() {
+            return this._paper;
+        },
+
+        getRenderTarget: function() {
+            return this._renderTarget;
+        },
+    });
+});

+ 110 - 0
kityminder-editor/src/core/patch.js

@@ -0,0 +1,110 @@
+/**
+ * @fileOverview
+ *
+ * 打补丁
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var Minder = require('./minder');
+
+    function insertNode(minder, info, parent, index) {
+        parent = minder.createNode(info.data, parent, index);
+        info.children.forEach(function(childInfo, index) {
+            insertNode(minder, childInfo, parent, index);
+        });
+        return parent;
+    }
+
+    function applyPatch(minder, patch) {
+        // patch.op - 操作,包括 remove, add, replace
+        // patch.path - 路径,如 '/root/children/1/data'
+        // patch.value - 数据,如 { text: "思路" }
+        var path = patch.path.split('/');
+        path.shift();
+
+        var changed = path.shift();
+        var node;
+
+        if (changed == 'root') {
+
+            var dataIndex = path.indexOf('data');
+            if (dataIndex > -1) {
+                changed = 'data';
+                var dataPath = path.splice(dataIndex + 1);
+                patch.dataPath = dataPath;
+            } else {
+                changed = 'node';
+            }
+
+            node = minder.getRoot();
+            var segment, index;
+            while (segment = path.shift()) {
+                if (segment == 'children') continue;
+                if (typeof index != 'undefined') node = node.getChild(index);
+                index = +segment;
+            }
+            patch.index = index;
+            patch.node = node;
+        }
+
+        var express = patch.express = [changed, patch.op].join('.');
+
+        switch (express) {
+            case 'theme.replace':
+                minder.useTheme(patch.value);
+                break;
+            case 'template.replace':
+                minder.useTemplate(patch.value);
+                break;
+            case 'node.add':
+                insertNode(minder, patch.value, patch.node, patch.index).renderTree();
+                minder.layout();
+                break;
+            case 'node.remove':
+                minder.removeNode(patch.node.getChild(patch.index));
+                minder.layout();
+                break;
+            case 'data.add':
+            case 'data.replace':
+            case 'data.remove':
+                var data = patch.node.data;
+                var field;
+                path = patch.dataPath.slice();
+                while (data && path.length > 1) {
+                    field = path.shift();
+                    if (field in data) {
+                        data = data[field];
+                    } else if (patch.op != 'remove') {
+                        data = data[field] = {};
+                    }
+                }
+                if (data) {
+                    field = path.shift();
+                    data[field] = patch.value;
+                }
+                if (field == 'expandState') {
+                    node.renderTree();
+                } else {
+                    node.render();
+                }
+                minder.layout();
+        }
+
+        minder.fire('patch', { 'patch' : patch } );
+    }
+
+    kity.extendClass(Minder, {
+        applyPatches: function(patches) {
+            for (var i = 0; i < patches.length; i++) {
+                applyPatch(this, patches[i]);
+            }
+
+            this.fire('contentchange');
+            return this;
+        }
+    });
+
+});

+ 214 - 0
kityminder-editor/src/core/promise.js

@@ -0,0 +1,214 @@
+define(function(require, exports, module) {
+    /*!
+    **  Thenable -- Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable
+    **  Copyright (c) 2013-2014 Ralf S. Engelschall <http://engelschall.com>
+    **  Licensed under The MIT License <http://opensource.org/licenses/MIT>
+    **  Source-Code distributed on <http://github.com/rse/thenable>
+    */
+
+    /*  promise states [Promises/A+ 2.1]  */
+    var STATE_PENDING   = 0;                                         /*  [Promises/A+ 2.1.1]  */
+    var STATE_FULFILLED = 1;                                         /*  [Promises/A+ 2.1.2]  */
+    var STATE_REJECTED  = 2;                                         /*  [Promises/A+ 2.1.3]  */
+
+    /*  promise object constructor  */
+    var Promise = function(executor) {
+        /*  optionally support non-constructor/plain-function call  */
+        if (!(this instanceof Promise))
+            return new Promise(executor);
+
+        /*  initialize object  */
+        this.id           = 'Thenable/1.0.7';
+        this.state        = STATE_PENDING; /*  initial state  */
+        this.fulfillValue = undefined;     /*  initial value  */     /*  [Promises/A+ 1.3, 2.1.2.2]  */
+        this.rejectReason = undefined;     /*  initial reason */     /*  [Promises/A+ 1.5, 2.1.3.2]  */
+        this.onFulfilled  = [];            /*  initial handlers  */
+        this.onRejected   = [];            /*  initial handlers  */
+
+        /*  support optional executor function  */
+        if (typeof executor === 'function')
+            executor.call(this, this.fulfill.bind(this), this.reject.bind(this));
+    };
+
+    /*  Promise API methods  */
+    Promise.prototype = {
+        /*  promise resolving methods  */
+        fulfill: function(value) { return deliver(this, STATE_FULFILLED, 'fulfillValue', value); },
+        reject:  function(value) { return deliver(this, STATE_REJECTED,  'rejectReason', value); },
+
+        /*  'The then Method' [Promises/A+ 1.1, 1.2, 2.2]  */
+        then: function(onFulfilled, onRejected) {
+            var curr = this;
+            var next = new Promise();                                    /*  [Promises/A+ 2.2.7]  */
+            curr.onFulfilled.push(
+                resolver(onFulfilled, next, 'fulfill'));             /*  [Promises/A+ 2.2.2/2.2.6]  */
+            curr.onRejected.push(
+                resolver(onRejected,  next, 'reject'));             /*  [Promises/A+ 2.2.3/2.2.6]  */
+            execute(curr);
+            return next;                                       /*  [Promises/A+ 2.2.7, 3.3]  */
+        }
+    };
+
+    Promise.all = function (arr) {
+        return new Promise(function(resolve, reject) {
+            var len = arr.length,
+                i = 0,
+                res = 0,
+                results = [];
+
+            if (len === 0) {
+                resolve(results);
+            }
+
+            while (i < len) {
+                arr[i].then(
+                    function (result) {
+                        results.push(result);
+                        if (++res === len) {
+                            resolve(results);
+                        }
+                    },
+                    function (val) {
+                        reject(val);
+                    }
+                );
+                i++;
+            }
+        });
+    };
+
+    /*  deliver an action  */
+    var deliver = function(curr, state, name, value) {
+        if (curr.state === STATE_PENDING) {
+            curr.state = state;                                      /*  [Promises/A+ 2.1.2.1, 2.1.3.1]  */
+            curr[name] = value;                                      /*  [Promises/A+ 2.1.2.2, 2.1.3.2]  */
+            execute(curr);
+        }
+        return curr;
+    };
+
+    /*  execute all handlers  */
+    var execute = function(curr) {
+        if (curr.state === STATE_FULFILLED)
+            execute_handlers(curr, 'onFulfilled', curr.fulfillValue);
+        else if (curr.state === STATE_REJECTED)
+            execute_handlers(curr, 'onRejected',  curr.rejectReason);
+    };
+
+    /*  execute particular set of handlers  */
+    var execute_handlers = function(curr, name, value) {
+        /* global process: true */
+        /* global setImmediate: true */
+        /* global setTimeout: true */
+
+        /*  short-circuit processing  */
+        if (curr[name].length === 0)
+            return;
+
+        /*  iterate over all handlers, exactly once  */
+        var handlers = curr[name];
+        curr[name] = [];                                             /*  [Promises/A+ 2.2.2.3, 2.2.3.3]  */
+        var func = function() {
+            for (var i = 0; i < handlers.length; i++)
+                handlers[i](value);                                  /*  [Promises/A+ 2.2.5]  */
+        };
+
+        /*  execute procedure asynchronously  */                     /*  [Promises/A+ 2.2.4, 3.1]  */
+        if (typeof process === 'object' && typeof process.nextTick === 'function')
+            process.nextTick(func);
+        else if (typeof setImmediate === 'function')
+            setImmediate(func);
+        else
+            setTimeout(func, 0);
+    };
+
+    /*  generate a resolver function */
+    var resolver = function(cb, next, method) {
+        return function(value) {
+            if (typeof cb !== 'function')                            /*  [Promises/A+ 2.2.1, 2.2.7.3, 2.2.7.4]  */
+                next[method].call(next, value);                      /*  [Promises/A+ 2.2.7.3, 2.2.7.4]  */
+            else {
+                var result;
+                try {
+                    if (value instanceof Promise) {
+                        result = value.then(cb);
+                    }
+                    else result = cb(value);
+                }                          /*  [Promises/A+ 2.2.2.1, 2.2.3.1, 2.2.5, 3.2]  */
+                catch (e) {
+                    next.reject(e);                                  /*  [Promises/A+ 2.2.7.2]  */
+                    return;
+                }
+                resolve(next, result);                               /*  [Promises/A+ 2.2.7.1]  */
+            }
+        };
+    };
+
+    /*  'Promise Resolution Procedure'  */                           /*  [Promises/A+ 2.3]  */
+    var resolve = function(promise, x) {
+        /*  sanity check arguments  */                               /*  [Promises/A+ 2.3.1]  */
+        if (promise === x) {
+            promise.reject(new TypeError('cannot resolve promise with itself'));
+            return;
+        }
+
+        /*  surgically check for a 'then' method
+            (mainly to just call the 'getter' of 'then' only once)  */
+        var then;
+        if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
+            try { then = x.then; }                                   /*  [Promises/A+ 2.3.3.1, 3.5]  */
+            catch (e) {
+                promise.reject(e);                                   /*  [Promises/A+ 2.3.3.2]  */
+                return;
+            }
+        }
+
+        /*  handle own Thenables    [Promises/A+ 2.3.2]
+            and similar 'thenables' [Promises/A+ 2.3.3]  */
+        if (typeof then === 'function') {
+            var resolved = false;
+            try {
+                /*  call retrieved 'then' method */                  /*  [Promises/A+ 2.3.3.3]  */
+                then.call(x,
+                    /*  resolvePromise  */                           /*  [Promises/A+ 2.3.3.3.1]  */
+                    function(y) {
+                        if (resolved) return; resolved = true;       /*  [Promises/A+ 2.3.3.3.3]  */
+                        if (y === x)                                 /*  [Promises/A+ 3.6]  */
+                            promise.reject(new TypeError('circular thenable chain'));
+                        else
+                            resolve(promise, y);
+                    },
+
+                    /*  rejectPromise  */                            /*  [Promises/A+ 2.3.3.3.2]  */
+                    function(r) {
+                        if (resolved) return; resolved = true;       /*  [Promises/A+ 2.3.3.3.3]  */
+                        promise.reject(r);
+                    }
+                );
+            }
+            catch (e) {
+                if (!resolved)                                       /*  [Promises/A+ 2.3.3.3.3]  */
+                    promise.reject(e);                               /*  [Promises/A+ 2.3.3.3.4]  */
+            }
+            return;
+        }
+
+        /*  handle other values  */
+        promise.fulfill(x);                                          /*  [Promises/A+ 2.3.4, 2.3.3.4]  */
+    };
+
+    Promise.resolve = function(value) {
+        return new Promise(function(resolve) {
+            resolve(value);
+        });
+    };
+
+    Promise.reject = function(reason) {
+        return new Promise(function(resolve, reject) {
+            reject(reason);
+        });
+    };
+
+    /*  export API  */
+    module.exports = Promise;
+});

+ 63 - 0
kityminder-editor/src/core/readonly.js

@@ -0,0 +1,63 @@
+/**
+ * @fileOverview
+ *
+ * 只读模式支持
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var Minder = require('./minder');
+    var MinderEvent = require('./event');
+
+    Minder.registerInitHook(function(options) {
+        if (options.readOnly) {
+            this.setDisabled();
+        }
+    });
+
+    kity.extendClass(Minder, {
+
+        disable: function() {
+            var me = this;
+            //禁用命令
+            me.bkqueryCommandState = me.queryCommandState;
+            me.bkqueryCommandValue = me.queryCommandValue;
+            me.queryCommandState = function(type) {
+                var cmd = this._getCommand(type);
+                if (cmd && cmd.enableReadOnly) {
+                    return me.bkqueryCommandState.apply(me, arguments);
+                }
+                return -1;
+            };
+            me.queryCommandValue = function(type) {
+                var cmd = this._getCommand(type);
+                if (cmd && cmd.enableReadOnly) {
+                    return me.bkqueryCommandValue.apply(me, arguments);
+                }
+                return null;
+            };
+            this.setStatus('readonly');
+            me._interactChange();
+        },
+
+        enable: function() {
+            var me = this;
+
+            if (me.bkqueryCommandState) {
+                me.queryCommandState = me.bkqueryCommandState;
+                delete me.bkqueryCommandState;
+            }
+            if (me.bkqueryCommandValue) {
+                me.queryCommandValue = me.bkqueryCommandValue;
+                delete me.bkqueryCommandValue;
+            }
+
+            this.setStatus('normal');
+
+            me._interactChange();
+        }
+    });
+});

+ 261 - 0
kityminder-editor/src/core/render.js

@@ -0,0 +1,261 @@
+define(function(require, exports, module) {
+
+    var kity = require('./kity');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+
+    var Renderer = kity.createClass('Renderer', {
+        constructor: function(node) {
+            this.node = node;
+        },
+
+        create: function(node) {
+            throw new Error('Not implement: Renderer.create()');
+        },
+
+        shouldRender: function(node) {
+            return true;
+        },
+
+        watchChange: function(data) {
+            var changed;
+
+            if (this.watchingData === undefined) {
+                changed = true;
+            } else if (this.watchingData != data) {
+                changed = true;
+            } else {
+                changed = false;
+            }
+
+            this.watchingData = data;
+        },
+
+        shouldDraw: function(node) {
+            return true;
+        },
+
+        update: function(shape, node, box) {
+            if (this.shouldDraw()) this.draw(shape, node);
+            return this.place(shape, node, box);
+        },
+
+        draw: function(shape, node) {
+            throw new Error('Not implement: Renderer.draw()');
+        },
+
+        place: function(shape, node, box) {
+            throw new Error('Not implement: Renderer.place()');
+        },
+
+        getRenderShape: function() {
+            return this._renderShape || null;
+        },
+
+        setRenderShape: function(shape) {
+            this._renderShape = shape;
+        }
+    });
+
+    function createMinderExtension() {
+
+        function createRendererForNode(node, registered) {
+            var renderers = [];
+
+            ['center', 'left', 'right', 'top', 'bottom', 'outline', 'outside'].forEach(function(section) {
+                var before = 'before' + section;
+                var after = 'after' + section;
+
+                if (registered[before]) {
+                    renderers = renderers.concat(registered[before]);
+                }
+                if (registered[section]) {
+                    renderers = renderers.concat(registered[section]);
+                }
+                if (registered[after]) {
+                    renderers = renderers.concat(registered[after]);
+                }
+            });
+
+            node._renderers = renderers.map(function(Renderer) {
+                return new Renderer(node);
+            });
+        }
+
+        return {
+            renderNodeBatch: function(nodes) {
+                var rendererClasses = this._rendererClasses;
+                var lastBoxes = [];
+                var rendererCount = 0;
+                var i, j, renderer, node;
+
+                if (!nodes.length) return;
+
+                for (j = 0; j < nodes.length; j++) {
+                    node = nodes[j];
+                    if (!node._renderers) {
+                        createRendererForNode(node, rendererClasses);
+                    }
+                    node._contentBox = new kity.Box();
+                    this.fire('beforerender', {
+                        node: node
+                    });
+                }
+
+                // 所有节点渲染器数量是一致的
+                rendererCount = nodes[0]._renderers.length;
+
+                for (i = 0; i < rendererCount; i++) {
+
+                    // 获取延迟盒子数据
+                    for (j = 0; j < nodes.length; j++) {
+                        if (typeof(lastBoxes[j]) == 'function') {
+                            lastBoxes[j] = lastBoxes[j]();
+                        }
+                        if (!(lastBoxes[j] instanceof kity.Box)) {
+                            lastBoxes[j] = new kity.Box(lastBoxes[j]);
+                        }
+                    }
+
+                    for (j = 0; j < nodes.length; j++) {
+                        node = nodes[j];
+                        renderer = node._renderers[i];
+
+                        // 合并盒子
+                        if (lastBoxes[j]) {
+                            node._contentBox = node._contentBox.merge(lastBoxes[j]);
+                            renderer.contentBox = lastBoxes[j];
+                        }
+
+                        // 判断当前上下文是否应该渲染
+                        if (renderer.shouldRender(node)) {
+
+                            // 应该渲染,但是渲染图形没创建过,需要创建
+                            if (!renderer.getRenderShape()) {
+                                renderer.setRenderShape(renderer.create(node));
+                                if (renderer.bringToBack) {
+                                    node.getRenderContainer().prependShape(renderer.getRenderShape());
+                                } else {
+                                    node.getRenderContainer().appendShape(renderer.getRenderShape());
+                                }
+                            }
+
+                            // 强制让渲染图形显示
+                            renderer.getRenderShape().setVisible(true);
+
+                            // 更新渲染图形
+                            lastBoxes[j] = renderer.update(renderer.getRenderShape(), node, node._contentBox);
+                        }
+
+                        // 如果不应该渲染,但是渲染图形创建过了,需要隐藏起来
+                        else if (renderer.getRenderShape()) {
+                            renderer.getRenderShape().setVisible(false);
+                            lastBoxes[j] = null;
+                        }
+                    }
+                }
+
+                for (j = 0; j < nodes.length; j++) {
+                    this.fire('noderender', {
+                        node: nodes[j]
+                    });
+                }
+            },
+
+            renderNode: function(node) {
+                var rendererClasses = this._rendererClasses;
+                var i, latestBox, renderer;
+
+                if (!node._renderers) {
+                    createRendererForNode(node, rendererClasses);
+                }
+
+                this.fire('beforerender', {
+                    node: node
+                });
+
+                node._contentBox = new kity.Box();
+
+                node._renderers.forEach(function(renderer) {
+
+                    // 判断当前上下文是否应该渲染
+                    if (renderer.shouldRender(node)) {
+
+                        // 应该渲染,但是渲染图形没创建过,需要创建
+                        if (!renderer.getRenderShape()) {
+                            renderer.setRenderShape(renderer.create(node));
+                            if (renderer.bringToBack) {
+                                node.getRenderContainer().prependShape(renderer.getRenderShape());
+                            } else {
+                                node.getRenderContainer().appendShape(renderer.getRenderShape());
+                            }
+                        }
+
+                        // 强制让渲染图形显示
+                        renderer.getRenderShape().setVisible(true);
+
+                        // 更新渲染图形
+                        latestBox = renderer.update(renderer.getRenderShape(), node, node._contentBox);
+
+                        if (typeof(latestBox) == 'function') latestBox = latestBox();
+
+                        // 合并渲染区域
+                        if (latestBox) {
+                            node._contentBox = node._contentBox.merge(latestBox);
+                            renderer.contentBox = latestBox;
+                        }
+                    }
+
+                    // 如果不应该渲染,但是渲染图形创建过了,需要隐藏起来
+                    else if (renderer.getRenderShape()) {
+                        renderer.getRenderShape().setVisible(false);
+                    }
+
+                });
+
+                this.fire('noderender', {
+                    node: node
+                });
+            }
+        };
+    }
+
+    kity.extendClass(Minder, createMinderExtension());
+
+    kity.extendClass(MinderNode, {
+        render: function() {
+            if (!this.attached) return;
+            this.getMinder().renderNode(this);
+            return this;
+        },
+        renderTree: function() {
+            if (!this.attached) return;
+            var list = [];
+            this.traverse(function(node) {
+                list.push(node);
+            });
+            this.getMinder().renderNodeBatch(list);
+            return this;
+        },
+        getRenderer: function(type) {
+            var rs = this._renderers;
+            if (!rs) return null;
+            for (var i = 0; i < rs.length; i++) {
+                if (rs[i].getType() == type) return rs[i];
+            }
+            return null;
+        },
+        getContentBox: function() {
+            //if (!this._contentBox) this.render();
+            return this.parent && this.parent.isCollapsed() ? new kity.Box() : (this._contentBox || new kity.Box());
+        },
+        getRenderBox: function(rendererType, refer) {
+            var renderer = rendererType && this.getRenderer(rendererType);
+            var contentBox = renderer ? renderer.contentBox : this.getContentBox();
+            var ctm = kity.Matrix.getCTM(this.getRenderContainer(), refer || 'paper');
+            return ctm.transformBox(contentBox);
+        }
+    });
+
+    module.exports = Renderer;
+});

+ 146 - 0
kityminder-editor/src/core/select.js

@@ -0,0 +1,146 @@
+
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+
+    Minder.registerInitHook(function() {
+        this._initSelection();
+    });
+
+    // 选区管理
+    kity.extendClass(Minder, {
+        _initSelection: function() {
+            this._selectedNodes = [];
+        },
+        renderChangedSelection: function(last) {
+            var current = this.getSelectedNodes();
+            var changed = [];
+
+            current.forEach(function(node) {
+                if (last.indexOf(node) == -1) {
+                    changed.push(node);
+                }
+            });
+
+            last.forEach(function(node) {
+                if (current.indexOf(node) == -1) {
+                    changed.push(node);
+                }
+            });
+
+            if (changed.length) {
+                this._interactChange();
+                this.fire('selectionchange');
+            }
+            while (changed.length) {
+                changed.shift().render();
+            }
+        },
+        getSelectedNodes: function() {
+            //不能克隆返回,会对当前选区操作,从而影响querycommand
+            return this._selectedNodes;
+        },
+        getSelectedNode: function() {
+            return this.getSelectedNodes()[0] || null;
+        },
+        removeAllSelectedNodes: function() {
+            var me = this;
+            var last = this._selectedNodes.splice(0);
+            this._selectedNodes = [];
+            this.renderChangedSelection(last);
+            return this.fire('selectionclear');
+        },
+        removeSelectedNodes: function(nodes) {
+            var me = this;
+            var last = this._selectedNodes.slice(0);
+            nodes = utils.isArray(nodes) ? nodes : [nodes];
+
+            nodes.forEach(function(node) {
+                var index;
+                if ((index = me._selectedNodes.indexOf(node)) === -1) return;
+                me._selectedNodes.splice(index, 1);
+            });
+
+            this.renderChangedSelection(last);
+            return this;
+        },
+        select: function(nodes, isSingleSelect) {
+            var lastSelect = this.getSelectedNodes().slice(0);
+            if (isSingleSelect) {
+                this._selectedNodes = [];
+            }
+            var me = this;
+            nodes = utils.isArray(nodes) ? nodes : [nodes];
+            nodes.forEach(function(node) {
+                if (me._selectedNodes.indexOf(node) !== -1) return;
+                me._selectedNodes.unshift(node);
+            });
+            this.renderChangedSelection(lastSelect);
+            return this;
+        },
+        selectById: function(ids, isSingleSelect) {
+            ids = utils.isArray(ids) ? ids : [ids];
+            var nodes = this.getNodesById(ids);
+            return this.select(nodes, isSingleSelect);
+        },
+        //当前选区中的节点在给定的节点范围内的保留选中状态,
+        //没在给定范围的取消选中,给定范围中的但没在当前选中范围的也做选中效果
+        toggleSelect: function(node) {
+            if (utils.isArray(node)) {
+                node.forEach(this.toggleSelect.bind(this));
+            } else {
+                if (node.isSelected()) this.removeSelectedNodes(node);
+                else this.select(node);
+            }
+            return this;
+        },
+
+        isSingleSelect: function() {
+            return this._selectedNodes.length == 1;
+        },
+
+        getSelectedAncestors: function(includeRoot) {
+            var nodes = this.getSelectedNodes().slice(0),
+                ancestors = [],
+                judge;
+
+            // 根节点不参与计算
+            var rootIndex = nodes.indexOf(this.getRoot());
+            if (~rootIndex && !includeRoot) {
+                nodes.splice(rootIndex, 1);
+            }
+
+            // 判断 nodes 列表中是否存在 judge 的祖先
+            function hasAncestor(nodes, judge) {
+                for (var i = nodes.length - 1; i >= 0; --i) {
+                    if (nodes[i].isAncestorOf(judge)) return true;
+                }
+                return false;
+            }
+
+            // 按照拓扑排序
+            nodes.sort(function(node1, node2) {
+                return node1.getLevel() - node2.getLevel();
+            });
+
+            // 因为是拓扑有序的,所以只需往上查找
+            while ((judge = nodes.pop())) {
+                if (!hasAncestor(nodes, judge)) {
+                    ancestors.push(judge);
+                }
+            }
+
+            return ancestors;
+        }
+    });
+
+    kity.extendClass(MinderNode, {
+        isSelected: function() {
+            var minder = this.getMinder();
+            return minder && minder.getSelectedNodes().indexOf(this) != -1;
+        }
+    });
+
+});

+ 154 - 0
kityminder-editor/src/core/shortcut.js

@@ -0,0 +1,154 @@
+/**
+ * @fileOverview
+ *
+ * 添加快捷键支持
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var keymap = require('./keymap');
+    var Minder = require('./minder');
+    var MinderEvent = require('./event');
+
+    /**
+     * 计算包含 meta 键的 keycode
+     *
+     * @param  {String|KeyEvent} unknown
+     */
+    function getMetaKeyCode(unknown) {
+        var CTRL_MASK = 0x1000;
+        var ALT_MASK = 0x2000;
+        var SHIFT_MASK = 0x4000;
+        var metaKeyCode = 0;
+
+        if (typeof(unknown) == 'string') {
+            // unknown as string
+            unknown.toLowerCase().split(/\+\s*/).forEach(function(name) {
+                switch(name) {
+                    case 'ctrl':
+                    case 'cmd':
+                        metaKeyCode |= CTRL_MASK;
+                        break;
+                    case 'alt':
+                        metaKeyCode |= ALT_MASK;
+                        break;
+                    case 'shift':
+                        metaKeyCode |= SHIFT_MASK;
+                        break;
+                    default:
+                        metaKeyCode |= keymap[name];
+                }
+            });
+        } else {
+            // unknown as key event
+            if (unknown.ctrlKey || unknown.metaKey) {
+                metaKeyCode |= CTRL_MASK;
+            }
+            if (unknown.altKey) {
+                metaKeyCode |= ALT_MASK;
+            }
+            if (unknown.shiftKey) {
+                metaKeyCode |= SHIFT_MASK;
+            }
+            metaKeyCode |= unknown.keyCode;
+        }
+
+        return metaKeyCode;
+    }
+    kity.extendClass(MinderEvent, {
+        isShortcutKey: function(keyCombine) {
+            var keyEvent = this.originEvent;
+            if (!keyEvent) return false;
+
+            return getMetaKeyCode(keyCombine) == getMetaKeyCode(keyEvent);
+        }
+    });
+
+    Minder.registerInitHook(function() {
+        this._initShortcutKey();
+    });
+
+    kity.extendClass(Minder, {
+
+        _initShortcutKey: function() {
+            this._bindShortcutKeys();
+        },
+
+        _bindShortcutKeys: function() {
+            var map = this._shortcutKeys = {};
+            var has = 'hasOwnProperty';
+            this.on('keydown', function(e) {
+                for (var keys in map) {
+                    if (!map[has](keys)) continue;
+                    if (e.isShortcutKey(keys)) {
+                        var fn = map[keys];
+                        if (fn.__statusCondition && fn.__statusCondition != this.getStatus()) return;
+                        fn();
+                        e.preventDefault();
+                    }
+                }
+            });
+        },
+
+        addShortcut: function(keys, fn) {
+            var binds = this._shortcutKeys;
+            keys.split(/\|\s*/).forEach(function(combine) {
+                var parts = combine.split('::');
+                var status;
+                if (parts.length > 1) {
+                    combine = parts[1];
+                    status = parts[0];
+                    fn.__statusCondition = status;
+                }
+                binds[combine] = fn;
+            });
+        },
+
+        addCommandShortcutKeys: function(cmd, keys) {
+            var binds = this._commandShortcutKeys || (this._commandShortcutKeys = {});
+            var obj = {},
+                km = this;
+            if (keys) {
+                obj[cmd] = keys;
+            } else {
+                obj = cmd;
+            }
+
+            var minder = this;
+
+            utils.each(obj, function(keys, command) {
+
+                binds[command] = keys;
+
+                minder.addShortcut(keys, function execCommandByShortcut() {
+                    /**
+                     * 之前判断有问题,由 === 0 改为 !== -1
+                     * @editor Naixor
+                     * @Date 2015-12-2
+                     */
+                    if (minder.queryCommandState(command) !== -1) {
+                        minder.execCommand(command);
+                    }
+                });
+            });
+        },
+
+        getCommandShortcutKey: function(cmd) {
+            var binds = this._commandShortcutKeys;
+            return binds && binds[cmd] || null;
+        },
+        
+        /**
+         * @Desc: 添加一个判断是否支持原生Clipboard的变量,用于对ctrl + v和ctrl + c的处理
+         * @Editor: Naixor
+         * @Date: 2015.9.20
+         */
+        supportClipboardEvent: (function(window) {
+            return !!window.ClipboardEvent;
+        })(window)
+    });
+});

+ 60 - 0
kityminder-editor/src/core/status.js

@@ -0,0 +1,60 @@
+/**
+ * @fileOverview
+ *
+ * 状态切换控制
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var Minder = require('./minder');
+
+    var sf = ~window.location.href.indexOf('status');
+    var tf = ~window.location.href.indexOf('trace');
+
+    Minder.registerInitHook(function() {
+        this._initStatus();
+    });
+
+    kity.extendClass(Minder, {
+
+        _initStatus: function() {
+            this._status = 'normal';
+            this._rollbackStatus = 'normal';
+        },
+
+        setStatus: function(status, force) {
+            // 在 readonly 模式下,只有 force 为 true 才能切换回来
+            if (this._status == 'readonly' && !force) return this;
+            if (status != this._status) {
+                this._rollbackStatus = this._status;
+                this._status = status;
+                this.fire('statuschange', {
+                    lastStatus: this._rollbackStatus,
+                    currentStatus: this._status
+                });
+                if (sf) {
+                    /* global console: true */
+                    console.log(window.event.type, this._rollbackStatus, '->', this._status);
+                    if (tf) {
+                        console.trace();
+                    }
+                }
+            }
+            return this;
+        },
+
+        rollbackStatus: function() {
+            this.setStatus(this._rollbackStatus);
+        },
+        getRollbackStatus:function(){
+            return this._rollbackStatus;
+        },
+        getStatus: function() {
+            return this._status;
+        }
+    });
+
+});

+ 92 - 0
kityminder-editor/src/core/template.js

@@ -0,0 +1,92 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+    var Command = require('./command');
+    var MinderNode = require('./node');
+    var Module = require('./module');
+
+    var _templates = {};
+
+    function register(name, supports) {
+        _templates[name] = supports;
+    }
+    exports.register = register;
+
+    utils.extend(Minder, {
+        getTemplateList: function() {
+            return _templates;
+        }
+    });
+
+    kity.extendClass(Minder, (function() {
+        var originGetTheme = Minder.prototype.getTheme;
+        return {
+            useTemplate: function(name, duration) {
+                this.setTemplate(name);
+                this.refresh(duration || 800);
+            },
+
+            getTemplate: function() {
+                return this._template || 'default';
+            },
+
+            setTemplate: function(name) {
+                this._template = name || null;
+            },
+
+            getTemplateSupport: function(method) {
+                var supports = _templates[this.getTemplate()];
+                return supports && supports[method];
+            },
+
+            getTheme: function(node) {
+                var support = this.getTemplateSupport('getTheme') || originGetTheme;
+                return support.call(this, node);
+            }
+        };
+    })());
+
+
+    kity.extendClass(MinderNode, (function() {
+        var originGetLayout = MinderNode.prototype.getLayout;
+        var originGetConnect = MinderNode.prototype.getConnect;
+        return {
+            getLayout: function() {
+                var support = this.getMinder().getTemplateSupport('getLayout') || originGetLayout;
+                return support.call(this, this);
+            },
+
+            getConnect: function() {
+                var support = this.getMinder().getTemplateSupport('getConnect') || originGetConnect;
+                return support.call(this, this);
+            }
+        };
+    })());
+
+    Module.register('TemplateModule', {
+        /**
+         * @command Template
+         * @description 设置当前脑图的模板
+         * @param {string} name 模板名称
+         *    允许使用的模板可以使用 `kityminder.Minder.getTemplateList()` 查询
+         * @state
+         *   0: 始终可用
+         * @return 返回当前的模板名称
+         */
+        commands: {
+            'template': kity.createClass('TemplateCommand', {
+                base: Command,
+
+                execute: function(minder, name) {
+                    minder.useTemplate(name);
+                    minder.execCommand('camera');
+                },
+
+                queryValue: function(minder) {
+                    return minder.getTemplate() || 'default';
+                }
+            })
+        }
+    });
+});

+ 175 - 0
kityminder-editor/src/core/theme.js

@@ -0,0 +1,175 @@
+define(function(require, exports, module) {
+    var kity = require('./kity');
+    var utils = require('./utils');
+    var Minder = require('./minder');
+    var MinderNode = require('./node');
+    var Module = require('./module');
+    var Command = require('./command');
+
+    var cssLikeValueMatcher = {
+        left: function(value) {
+            return 3 in value && value[3] ||
+                1 in value && value[1] ||
+                value[0];
+        },
+        right: function(value) {
+            return 1 in value && value[1] || value[0];
+        },
+        top: function(value) {
+            return value[0];
+        },
+        bottom: function(value) {
+            return 2 in value && value[2] || value[0];
+        }
+    };
+
+    var _themes = {};
+
+    /**
+     * 注册一个主题
+     *
+     * @param  {String} name  主题的名称
+     * @param  {Plain} theme 主题的样式描述
+     *
+     * @example
+     *     Minder.registerTheme('default', {
+     *         'root-color': 'red',
+     *         'root-stroke': 'none',
+     *         'root-padding': [10, 20]
+     *     });
+     */
+    function register(name, theme) {
+        _themes[name] = theme;
+    }
+    exports.register = register;
+
+    utils.extend(Minder, {
+        getThemeList: function() {
+            return _themes;
+        }
+    });
+
+    kity.extendClass(Minder, {
+
+        /**
+         * 切换脑图实例上的主题
+         * @param  {String} name 要使用的主题的名称
+         */
+        useTheme: function(name) {
+
+            this.setTheme(name);
+            this.refresh(800);
+
+            return true;
+        },
+
+        setTheme: function(name) {
+            if (name && !_themes[name]) throw new Error('Theme ' + name + ' not exists!');
+            var lastTheme = this._theme;
+            this._theme = name || null;
+            var container = this.getRenderTarget();
+            if (container) {
+                container.classList.remove('km-theme-' + lastTheme);
+                if (name) {
+                    container.classList.add('km-theme-' + name);
+                }
+                container.style.background = this.getStyle('background');
+            }
+            this.fire('themechange', {
+                theme: name
+            });
+            return this;
+        },
+
+        /**
+         * 获取脑图实例上的当前主题
+         * @return {[type]} [description]
+         */
+        getTheme: function(node) {
+            return this._theme || this.getOption('defaultTheme') || 'fresh-blue';
+        },
+
+        getThemeItems: function(node) {
+            var theme = this.getTheme(node);
+            return _themes[this.getTheme(node)];
+        },
+
+        /**
+         * 获得脑图实例上的样式
+         * @param  {String} item 样式名称
+         */
+        getStyle: function(item, node) {
+            var items = this.getThemeItems(node);
+            var segment, dir, selector, value, matcher;
+
+            if (item in items) return items[item];
+
+            // 尝试匹配 CSS 数组形式的值
+            // 比如 item 为 'pading-left'
+            // theme 里有 {'padding': [10, 20]} 的定义,则可以返回 20
+            segment = item.split('-');
+            if (segment.length < 2) return null;
+
+            dir = segment.pop();
+            item = segment.join('-');
+
+            if (item in items) {
+                value = items[item];
+                if (utils.isArray(value) && (matcher = cssLikeValueMatcher[dir])) {
+                    return matcher(value);
+                }
+                if (!isNaN(value)) return value;
+            }
+
+            return null;
+        },
+
+        /**
+         * 获取指定节点的样式
+         * @param  {String} name 样式名称,可以不加节点类型的前缀
+         */
+        getNodeStyle: function(node, name) {
+            var value = this.getStyle(node.getType() + '-' + name, node);
+            return value !== null ? value : this.getStyle(name, node);
+        }
+    });
+
+    kity.extendClass(MinderNode, {
+        getStyle: function(name) {
+            return this.getMinder().getNodeStyle(this, name);
+        }
+    });
+
+    Module.register('Theme', {
+        defaultOptions: {
+            defaultTheme: 'fresh-blue'
+        },
+        commands: {
+            /**
+             * @command Theme
+             * @description 设置当前脑图的主题
+             * @param {string} name 主题名称
+             *    允许使用的主题可以使用 `kityminder.Minder.getThemeList()` 查询
+             * @state
+             *   0: 始终可用
+             * @return 返回当前的主题名称
+             */
+            'theme': kity.createClass('ThemeCommand', {
+                base: Command,
+
+                execute: function(km, name) {
+                    return km.useTheme(name);
+                },
+
+                queryValue: function(km) {
+                    return km.getTheme() || 'default';
+                }
+            })
+        }
+    });
+
+    Minder.registerInitHook(function() {
+        this.setTheme();
+    });
+
+});

+ 65 - 0
kityminder-editor/src/core/utils.js

@@ -0,0 +1,65 @@
+define(function(require, exports) {
+    var kity = require('./kity');
+    var uuidMap = {};
+
+    exports.extend = kity.Utils.extend.bind(kity.Utils);
+    exports.each = kity.Utils.each.bind(kity.Utils);
+
+    exports.uuid = function(group) {
+        uuidMap[group] = uuidMap[group] ? uuidMap[group] + 1 : 1;
+        return group + uuidMap[group];
+    };
+
+    exports.guid = function() {
+        return (+new Date() * 1e6 + Math.floor(Math.random() * 1e6)).toString(36);
+    };
+
+    exports.trim = function(str) {
+        return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, '');
+    };
+
+    exports.keys = function(plain) {
+        var keys = [];
+        for (var key in plain) {
+            if (plain.hasOwnProperty(key)) {
+                keys.push(key);
+            }
+        }
+        return keys;
+    };
+
+    exports.clone = function(source) {
+        return JSON.parse(JSON.stringify(source));
+    };
+
+    exports.comparePlainObject = function(a, b) {
+        return JSON.stringify(a) == JSON.stringify(b);
+    };
+
+    exports.encodeHtml = function(str, reg) {
+        return str ? str.replace(reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp);)?/g, function(a, b) {
+            if (b) {
+                return a;
+            } else {
+                return {
+                    '<': '&lt;',
+                    '&': '&amp;',
+                    '"': '&quot;',
+                    '>': '&gt;',
+                    '\'': '&#39;'
+                }[a];
+            }
+        }) : '';
+    };
+
+    exports.clearWhiteSpace = function(str) {
+        return str.replace(/[\u200b\t\r\n]/g, '');
+    };
+
+    exports.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object'], function(v) {
+        var toString = Object.prototype.toString;
+        exports['is' + v] = function(obj) {
+            return toString.apply(obj) == '[object ' + v + ']';
+        };
+    });
+});

+ 3 - 0
kityminder-editor/src/expose-kityminder.js

@@ -0,0 +1,3 @@
+define('expose-kityminder', function(require, exports, module) {
+    module.exports = window.kityminder = require('./kityminder');
+});

Разлика између датотеке није приказан због своје велике величине
+ 80 - 0
kityminder-editor/src/kityminder.css


+ 106 - 0
kityminder-editor/src/kityminder.js

@@ -0,0 +1,106 @@
+/**
+ * @fileOverview
+ *
+ * 默认导出(全部模块)
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+
+define(function(require, exports, module) {
+    var kityminder = {
+        version: require('./core/minder').version
+    };
+
+    // 核心导出,大写的部分导出类,小写的部分简单 require 一下
+    // 这里顺序是有讲究的,调整前先弄清楚依赖关系。
+    require('./core/utils');
+    kityminder.Minder = require('./core/minder');
+    kityminder.Command = require('./core/command');
+    kityminder.Node = require('./core/node');
+    require('./core/option');
+    require('./core/animate');
+    kityminder.Event = require('./core/event');
+    kityminder.data = require('./core/data');
+    require('./core/compatibility');
+    kityminder.KeyMap = require('./core/keymap');
+    require('./core/shortcut');
+    require('./core/status');
+    require('./core/paper');
+    require('./core/select');
+    require('./core/focus');
+    require('./core/keyreceiver');
+    kityminder.Module = require('./core/module');
+    require('./core/readonly');
+    kityminder.Render = require('./core/render');
+    kityminder.Connect = require('./core/connect');
+    kityminder.Layout = require('./core/layout');
+    kityminder.Theme = require('./core/theme');
+    kityminder.Template = require('./core/template');
+    kityminder.Promise = require('./core/promise');
+    require('./core/_boxv');
+    require('./core/patch');
+
+    // 模块依赖
+    require('./module/arrange');
+    require('./module/basestyle');
+    require('./module/clipboard');
+    require('./module/dragtree');
+    require('./module/expand');
+    require('./module/font');
+    require('./module/hyperlink');
+    require('./module/image');
+    require('./module/image-viewer');
+    require('./module/keynav');
+    require('./module/layout');
+    require('./module/node');
+    require('./module/note');
+    require('./module/outline');
+    require('./module/priority');
+    require('./module/progress');
+    require('./module/resource');
+    require('./module/select');
+    require('./module/style');
+    require('./module/text');
+    require('./module/view');
+    require('./module/zoom');
+
+    require('./protocol/json');
+    require('./protocol/text');
+    require('./protocol/markdown');
+    require('./protocol/svg');
+    require('./protocol/png');
+
+    require('./layout/mind');
+    require('./layout/btree');
+    require('./layout/filetree');
+    require('./layout/fish-bone-master');
+    require('./layout/fish-bone-slave');
+    require('./layout/tianpan');
+
+    require('./theme/default');
+    require('./theme/snow');
+    require('./theme/fresh');
+    require('./theme/fish');
+    require('./theme/snow');
+    require('./theme/wire');
+    require('./theme/tianpan');
+
+    require('./connect/arc');
+    require('./connect/arc_tp');
+    require('./connect/bezier');
+    require('./connect/fish-bone-master');
+    require('./connect/l');
+    require('./connect/poly');
+    require('./connect/under');
+
+    require('./template/default');
+    require('./template/structure');
+    require('./template/filetree');
+    require('./template/right');
+    require('./template/fish-bone');
+    require('./template/tianpan');
+
+    module.exports = kityminder;
+});

+ 143 - 0
kityminder-editor/src/layout/btree.js

@@ -0,0 +1,143 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Layout = require('../core/layout');
+
+    ['left', 'right', 'top', 'bottom'].forEach(registerLayoutForDirection);
+
+    function registerLayoutForDirection(name) {
+
+        var axis = (name == 'left' || name == 'right') ? 'x' : 'y';
+        var dir = (name == 'left' || name == 'top') ? -1 : 1;
+
+        var oppsite = {
+            'left': 'right',
+            'right': 'left',
+            'top': 'bottom',
+            'bottom': 'top',
+            'x': 'y',
+            'y': 'x'
+        };
+
+        function getOrderHint(node) {
+            var hint = [];
+            var box = node.getLayoutBox();
+            var offset = 5;
+
+            if (axis == 'x') {
+                hint.push({
+                    type: 'up',
+                    node: node,
+                    area: new kity.Box({
+                        x: box.x,
+                        y: box.top - node.getStyle('margin-top') - offset,
+                        width: box.width,
+                        height: node.getStyle('margin-top')
+                    }),
+                    path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
+                });
+
+                hint.push({
+                    type: 'down',
+                    node: node,
+                    area: new kity.Box({
+                        x: box.x,
+                        y: box.bottom + offset,
+                        width: box.width,
+                        height: node.getStyle('margin-bottom')
+                    }),
+                    path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
+                });
+            } else {
+                hint.push({
+                    type: 'up',
+                    node: node,
+                    area: new kity.Box({
+                        x: box.left - node.getStyle('margin-left') - offset,
+                        y: box.top,
+                        width: node.getStyle('margin-left'),
+                        height: box.height
+                    }),
+                    path: ['M', box.left - offset, box.top, 'L', box.left - offset, box.bottom]
+                });
+
+                hint.push({
+                    type: 'down',
+                    node: node,
+                    area: new kity.Box({
+                        x: box.right + offset,
+                        y: box.top,
+                        width: node.getStyle('margin-right'),
+                        height: box.height
+                    }),
+                    path: ['M', box.right + offset, box.top, 'L', box.right + offset, box.bottom]
+                });
+            }
+            return hint;
+        }
+
+        Layout.register(name, kity.createClass({
+
+            base: Layout,
+
+            doLayout: function(parent, children) {
+
+                var pbox = parent.getContentBox();
+
+                if (axis == 'x') {
+                    parent.setVertexOut(new kity.Point(pbox[name], pbox.cy));
+                    parent.setLayoutVectorOut(new kity.Vector(dir, 0));
+                } else {
+                    parent.setVertexOut(new kity.Point(pbox.cx, pbox[name]));
+                    parent.setLayoutVectorOut(new kity.Vector(0, dir));
+                }
+
+                if (!children.length) {
+                    return false;
+                }
+
+                children.forEach(function(child) {
+                    var cbox = child.getContentBox();
+                    child.setLayoutTransform(new kity.Matrix());
+
+                    if (axis == 'x') {
+                        child.setVertexIn(new kity.Point(cbox[oppsite[name]], cbox.cy));
+                        child.setLayoutVectorIn(new kity.Vector(dir, 0));
+                    } else {
+                        child.setVertexIn(new kity.Point(cbox.cx, cbox[oppsite[name]]));
+                        child.setLayoutVectorIn(new kity.Vector(0, dir));
+                    }
+                });
+
+                this.align(children, oppsite[name]);
+                this.stack(children, oppsite[axis]);
+
+                var bbox = this.getBranchBox(children);
+                var xAdjust = 0, yAdjust = 0;
+
+                if (axis == 'x') {
+                    xAdjust = pbox[name];
+                    xAdjust += dir * parent.getStyle('margin-' + name);
+                    xAdjust += dir * children[0].getStyle('margin-' + oppsite[name]);
+
+                    yAdjust = pbox.bottom;
+                    yAdjust -= pbox.height / 2;
+                    yAdjust -= bbox.height / 2;
+                    yAdjust -= bbox.y;
+                } else {
+                    xAdjust = pbox.right;
+                    xAdjust -= pbox.width / 2;
+                    xAdjust -= bbox.width / 2;
+                    xAdjust -= bbox.x;
+
+                    yAdjust = pbox[name];
+                    yAdjust += dir * parent.getStyle('margin-' + name);
+                    yAdjust += dir * children[0].getStyle('margin-' + oppsite[name]);
+                }
+
+                this.move(children, xAdjust, yAdjust);
+            },
+
+            getOrderHint: getOrderHint
+        }));
+    }
+});

+ 89 - 0
kityminder-editor/src/layout/filetree.js

@@ -0,0 +1,89 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Layout = require('../core/layout');
+
+    [-1, 1].forEach(registerLayoutForDir);
+
+    function registerLayoutForDir(dir) {
+        var name = 'filetree-' + (dir > 0 ? 'down' : 'up');
+
+        Layout.register(name, kity.createClass({
+            base: Layout,
+
+            doLayout: function(parent, children, round) {
+                var pBox = parent.getContentBox();
+                var indent = 20;
+
+                parent.setVertexOut(new kity.Point(pBox.left + indent, dir > 0 ? pBox.bottom : pBox.top));
+                parent.setLayoutVectorOut(new kity.Vector(0, dir));
+
+                if (!children.length) return;
+
+                children.forEach(function(child) {
+                    var cbox = child.getContentBox();
+                    child.setLayoutTransform(new kity.Matrix());
+
+                    child.setVertexIn(new kity.Point(cbox.left, cbox.cy));
+                    child.setLayoutVectorIn(new kity.Vector(1, 0));
+                });
+
+                this.align(children, 'left');
+                this.stack(children, 'y');
+
+                var xAdjust = 0;
+                xAdjust += pBox.left;
+                xAdjust += indent;
+                xAdjust += children[0].getStyle('margin-left');
+
+                var yAdjust = 0;
+
+                if (dir > 0) {
+                    yAdjust += pBox.bottom;
+                    yAdjust += parent.getStyle('margin-bottom');
+                    yAdjust += children[0].getStyle('margin-top');
+                } else {
+                    yAdjust -= this.getTreeBox(children).bottom;
+                    yAdjust += pBox.top;
+                    yAdjust -= parent.getStyle('margin-top');
+                    yAdjust -= children[0].getStyle('margin-bottom');
+                }
+
+                this.move(children, xAdjust, yAdjust);
+
+            },
+
+            getOrderHint: function(node) {
+                var hint = [];
+                var box = node.getLayoutBox();
+                var offset = node.getLevel() > 1 ? 3 : 5;
+
+                hint.push({
+                    type: 'up',
+                    node: node,
+                    area: new kity.Box({
+                        x: box.x,
+                        y: box.top - node.getStyle('margin-top') - offset,
+                        width: box.width,
+                        height: node.getStyle('margin-top')
+                    }),
+                    path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
+                });
+
+                hint.push({
+                    type: 'down',
+                    node: node,
+                    area: new kity.Box({
+                        x: box.x,
+                        y: box.bottom + offset,
+                        width: box.width,
+                        height: node.getStyle('margin-bottom')
+                    }),
+                    path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
+                });
+                return hint;
+            }
+        }));
+
+    }
+
+});

+ 68 - 0
kityminder-editor/src/layout/fish-bone-master.js

@@ -0,0 +1,68 @@
+/**
+ * @fileOverview
+ *
+ * 鱼骨图主骨架布局
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Layout = require('../core/layout');
+
+    Layout.register('fish-bone-master', kity.createClass('FishBoneMasterLayout', {
+        base: Layout,
+
+        doLayout: function(parent, children, round) {
+
+            var upPart = [],
+                downPart = [];
+
+            var child = children[0];
+            var pBox = parent.getContentBox();
+
+            parent.setVertexOut(new kity.Point(pBox.right, pBox.cy));
+            parent.setLayoutVectorOut(new kity.Vector(1, 0));
+
+            if (!child) return;
+
+            var cBox = child.getContentBox();
+            var pMarginRight = parent.getStyle('margin-right');
+            var cMarginLeft = child.getStyle('margin-left');
+            var cMarginTop = child.getStyle('margin-top');
+            var cMarginBottom = child.getStyle('margin-bottom');
+
+            children.forEach(function(child, index) {
+                child.setLayoutTransform(new kity.Matrix());
+                var cBox = child.getContentBox();
+
+                if (index % 2) {
+                    downPart.push(child);
+                    child.setVertexIn(new kity.Point(cBox.left, cBox.top));
+                    child.setLayoutVectorIn(new kity.Vector(1, 1));
+                }
+                else {
+                    upPart.push(child);
+                    child.setVertexIn(new kity.Point(cBox.left, cBox.bottom));
+                    child.setLayoutVectorIn(new kity.Vector(1, -1));
+                }
+
+            });
+
+            this.stack(upPart, 'x');
+            this.stack(downPart, 'x');
+
+            this.align(upPart, 'bottom');
+            this.align(downPart, 'top');
+
+            var xAdjust = pBox.right + pMarginRight + cMarginLeft;
+            var yAdjustUp = pBox.cy - cMarginBottom - parent.getStyle('margin-top');
+            var yAdjustDown = pBox.cy + cMarginTop + parent.getStyle('margin-bottom');
+
+            this.move(upPart, xAdjust, yAdjustUp);
+            this.move(downPart, xAdjust + cMarginLeft, yAdjustDown);
+        }
+    }));
+
+});

+ 72 - 0
kityminder-editor/src/layout/fish-bone-slave.js

@@ -0,0 +1,72 @@
+/**
+ * @fileOverview
+ *
+ *
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Layout = require('../core/layout');
+
+    Layout.register('fish-bone-slave', kity.createClass('FishBoneSlaveLayout', {
+        base: Layout,
+
+        doLayout: function (parent, children, round) {
+
+            var layout = this;
+            var abs = Math.abs;
+            var GOLD_CUT = 1 - 0.618;
+
+            var pBox = parent.getContentBox();
+            var vi = parent.getLayoutVectorIn();
+
+            parent.setLayoutVectorOut(vi);
+
+            var goldX = pBox.left + pBox.width * GOLD_CUT;
+            var pout = new kity.Point(goldX, vi.y > 0 ? pBox.bottom : pBox.top);
+            parent.setVertexOut(pout);
+
+            var child = children[0];
+            if (!child) return;
+
+            var cBox = child.getContentBox();
+
+            children.forEach(function(child, index) {
+                child.setLayoutTransform(new kity.Matrix());
+                child.setLayoutVectorIn(new kity.Vector(1, 0));
+                child.setVertexIn(new kity.Point(cBox.left, cBox.cy));
+            });
+
+            this.stack(children, 'y');
+            this.align(children, 'left');
+
+            var xAdjust = 0, yAdjust = 0;
+            xAdjust += pout.x;
+
+            if (parent.getLayoutVectorOut().y < 0) {
+                yAdjust -= this.getTreeBox(children).bottom;
+                yAdjust += parent.getContentBox().top;
+                yAdjust -= parent.getStyle('margin-top');
+                yAdjust -= child.getStyle('margin-bottom');
+            } else {
+                yAdjust += parent.getContentBox().bottom;
+                yAdjust += parent.getStyle('margin-bottom');
+                yAdjust += child.getStyle('margin-top');
+            }
+
+            this.move(children, xAdjust, yAdjust);
+
+            if (round == 2) {
+                children.forEach(function(child) {
+                    var m = child.getLayoutTransform();
+                    var cbox = child.getContentBox();
+                    var pin = m.transformPoint(new kity.Point(cbox.left, 0));
+                    layout.move([child], abs(pin.y - pout.y), 0);
+                });
+            }
+        }
+    }));
+});

+ 62 - 0
kityminder-editor/src/layout/mind.js

@@ -0,0 +1,62 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Layout = require('../core/layout');
+    var Minder = require('../core/minder');
+
+    Layout.register('mind', kity.createClass({
+        base: Layout,
+
+        doLayout: function(node, children) {
+            var layout = this;
+            var half = Math.ceil(node.children.length / 2);
+            var right = [];
+            var left = [];
+
+            children.forEach(function(child) {
+                if (child.getIndex() < half) right.push(child);
+                else left.push(child);
+            });
+
+            var leftLayout = Minder.getLayoutInstance('left');
+            var rightLayout = Minder.getLayoutInstance('right');
+
+            leftLayout.doLayout(node, left);
+            rightLayout.doLayout(node, right);
+
+            var box = node.getContentBox();
+            node.setVertexOut(new kity.Point(box.cx, box.cy));
+            node.setLayoutVectorOut(new kity.Vector(0, 0));
+        },
+
+        getOrderHint: function(node) {
+            var hint = [];
+            var box = node.getLayoutBox();
+            var offset = 5;
+
+            hint.push({
+                type: 'up',
+                node: node,
+                area: new kity.Box({
+                    x: box.x,
+                    y: box.top - node.getStyle('margin-top') - offset,
+                    width: box.width,
+                    height: node.getStyle('margin-top')
+                }),
+                path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
+            });
+
+            hint.push({
+                type: 'down',
+                node: node,
+                area: new kity.Box({
+                    x: box.x,
+                    y: box.bottom + offset,
+                    width: box.width,
+                    height: node.getStyle('margin-bottom')
+                }),
+                path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
+            });
+            return hint;
+        }
+    }));
+});

+ 76 - 0
kityminder-editor/src/layout/tianpan.js

@@ -0,0 +1,76 @@
+/**
+ * @fileOverview
+ *
+ * 天盘模板
+ *
+ * @author: along
+ * @copyright: bpd729@163.com, 2015
+ */
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Layout = require('../core/layout');
+    var Minder = require('../core/minder');
+
+    Layout.register('tianpan', kity.createClass({
+        base: Layout,
+
+        doLayout: function (parent, children) {
+            if (children.length == 0) return;
+
+            var layout = this;
+            var pbox = parent.getContentBox();
+
+            var x, y,box;
+            var _theta = 5;
+            var _r = Math.max(pbox.width, 50);
+            children.forEach(function (child, index) {
+                child.setLayoutTransform(new kity.Matrix());
+                box = layout.getTreeBox(child);
+                _r = Math.max(Math.max(box.width, box.height), _r);
+            })
+            _r = _r / 1.5 / Math.PI;
+
+            children.forEach(function (child, index) {
+                x = _r * (Math.cos(_theta) + Math.sin(_theta) * _theta);
+                y = _r * (Math.sin(_theta) - Math.cos(_theta) * _theta);
+
+                _theta += (0.9 - index * 0.02);
+                child.setLayoutVectorIn(new kity.Vector(1, 0));
+                child.setVertexIn(new kity.Point(pbox.cx, pbox.cy));
+                child.setLayoutTransform(new kity.Matrix());
+                layout.move([child], x, y);
+            });
+        },
+
+        getOrderHint: function (node) {
+            var hint = [];
+            var box = node.getLayoutBox();
+            var offset = 5;
+
+            hint.push({
+                type: 'up',
+                node: node,
+                area: {
+                    x: box.x,
+                    y: box.top - node.getStyle('margin-top') - offset,
+                    width: box.width,
+                    height: node.getStyle('margin-top')
+                },
+                path: ['M', box.x, box.top - offset, 'L', box.right, box.top - offset]
+            });
+
+            hint.push({
+                type: 'down',
+                node: node,
+                area: {
+                    x: box.x,
+                    y: box.bottom + offset,
+                    width: box.width,
+                    height: node.getStyle('margin-bottom')
+                },
+                path: ['M', box.x, box.bottom + offset, 'L', box.right, box.bottom + offset]
+            });
+            return hint;
+        }
+    }));
+});

+ 156 - 0
kityminder-editor/src/module/arrange.js

@@ -0,0 +1,156 @@
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+
+    kity.extendClass(MinderNode, {
+        arrange: function(index) {
+            var parent = this.parent;
+            if (!parent) return;
+            var sibling = parent.children;
+
+            if (index < 0 || index >= sibling.length) return;
+            sibling.splice(this.getIndex(), 1);
+            sibling.splice(index, 0, this);
+            return this;
+        }
+    });
+
+    function asc(nodeA, nodeB) {
+        return nodeA.getIndex() - nodeB.getIndex();
+    }
+    function desc(nodeA, nodeB) {
+        return -asc(nodeA, nodeB);
+    }
+
+    function canArrange(km) {
+        var selected = km.getSelectedNode();
+        return selected && selected.parent && selected.parent.children.length > 1;
+    }
+
+
+    /**
+     * @command ArrangeUp
+     * @description 向上调整选中节点的位置
+     * @shortcut Alt + Up
+     * @state
+     *    0: 当前选中了具有相同父亲的节点
+     *   -1: 其它情况
+     */
+    var ArrangeUpCommand = kity.createClass('ArrangeUpCommand', {
+        base: Command,
+
+        execute: function(km) {
+            var nodes = km.getSelectedNodes();
+            nodes.sort(asc);
+            var lastIndexes = nodes.map(function(node) {
+                return node.getIndex();
+            });
+            nodes.forEach(function(node, index) {
+                node.arrange(lastIndexes[index] - 1);
+            });
+            km.layout(300);
+        },
+
+        queryState: function(km) {
+            var selected = km.getSelectedNode();
+            return selected ? 0 : -1;
+        }
+    });
+
+    /**
+     * @command ArrangeDown
+     * @description 向下调整选中节点的位置
+     * @shortcut Alt + Down
+     * @state
+     *    0: 当前选中了具有相同父亲的节点
+     *   -1: 其它情况
+     */
+    var ArrangeDownCommand = kity.createClass('ArrangeUpCommand', {
+        base: Command,
+
+        execute: function(km) {
+            var nodes = km.getSelectedNodes();
+            nodes.sort(desc);
+            var lastIndexes = nodes.map(function(node) {
+                return node.getIndex();
+            });
+            nodes.forEach(function(node, index) {
+                node.arrange(lastIndexes[index] + 1);
+            });
+            km.layout(300);
+        },
+
+        queryState: function(km) {
+            var selected = km.getSelectedNode();
+            return selected ? 0 : -1;
+        }
+    });
+
+    /**
+     * @command Arrange
+     * @description 调整选中节点的位置
+     * @param {number} index 调整后节点的新位置
+     * @state
+     *    0: 当前选中了具有相同父亲的节点
+     *   -1: 其它情况
+     */
+    var ArrangeCommand = kity.createClass('ArrangeCommand', {
+        base: Command,
+
+        execute: function(km, index) {
+            var nodes = km.getSelectedNodes().slice();
+
+            if (!nodes.length) return;
+
+            var ancestor = MinderNode.getCommonAncestor(nodes);
+
+            if (ancestor != nodes[0].parent) return;
+
+            var indexed = nodes.map(function(node) {
+                return {
+                    index: node.getIndex(),
+                    node: node
+                };
+            });
+
+            var asc = Math.min.apply(Math, indexed.map(function(one) { return one.index; })) >= index;
+
+            indexed.sort(function(a, b) {
+                return asc ? (b.index - a.index) : (a.index - b.index);
+            });
+
+            indexed.forEach(function(one) {
+                one.node.arrange(index);
+            });
+
+            km.layout(300);
+        },
+
+        queryState: function(km) {
+            var selected = km.getSelectedNode();
+            return selected ? 0 : -1;
+        }
+    });
+
+    Module.register('ArrangeModule', {
+        commands: {
+            'arrangeup': ArrangeUpCommand,
+            'arrangedown': ArrangeDownCommand,
+            'arrange': ArrangeCommand
+        },
+        contextmenu: [{
+            command: 'arrangeup'
+        }, {
+            command: 'arrangedown'
+        }, {
+            divider: true
+        }],
+        commandShortcutKeys: {
+            'arrangeup': 'normal::alt+Up',
+            'arrangedown': 'normal::alt+Down'
+        }
+    });
+});

+ 126 - 0
kityminder-editor/src/module/basestyle.js

@@ -0,0 +1,126 @@
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+
+    var TextRenderer = require('./text');
+
+    Module.register('basestylemodule', function() {
+        var km = this;
+
+        function getNodeDataOrStyle(node, name) {
+            return node.getData(name) || node.getStyle(name);
+        }
+
+        TextRenderer.registerStyleHook(function(node, textGroup) {
+
+            var fontWeight = getNodeDataOrStyle(node,'font-weight');
+            var fontStyle = getNodeDataOrStyle(node, 'font-style');
+            var styleHash = [fontWeight, fontStyle].join('/');
+
+            textGroup.eachItem(function(index,item) {
+                item.setFont({
+                    'weight': fontWeight,
+                    'style': fontStyle
+                });
+            });
+
+        });
+        return {
+            'commands': {
+                /**
+                 * @command Bold
+                 * @description 加粗选中的节点
+                 * @shortcut Ctrl + B
+                 * @state
+                 *   0: 当前有选中的节点
+                 *  -1: 当前没有选中的节点
+                 *   1: 当前已选中的节点已加粗
+                 */
+                'bold': kity.createClass('boldCommand', {
+                    base: Command,
+
+                    execute: function(km) {
+
+                        var nodes = km.getSelectedNodes();
+                        if (this.queryState('bold') == 1) {
+                            nodes.forEach(function(n) {
+                                n.setData('font-weight').render();
+                            });
+                        } else {
+                            nodes.forEach(function(n) {
+                                n.setData('font-weight', 'bold').render();
+                            });
+                        }
+                        km.layout();
+                    },
+                    queryState: function() {
+                        var nodes = km.getSelectedNodes(),
+                            result = 0;
+                        if (nodes.length === 0) {
+                            return -1;
+                        }
+                        nodes.forEach(function(n) {
+                            if (n && n.getData('font-weight')) {
+                                result = 1;
+                                return false;
+                            }
+                        });
+                        return result;
+                    }
+                }),
+                /**
+                 * @command Italic
+                 * @description 加斜选中的节点
+                 * @shortcut Ctrl + I
+                 * @state
+                 *   0: 当前有选中的节点
+                 *  -1: 当前没有选中的节点
+                 *   1: 当前已选中的节点已加斜
+                 */
+                'italic': kity.createClass('italicCommand', {
+                    base: Command,
+
+                    execute: function(km) {
+
+                        var nodes = km.getSelectedNodes();
+                        if (this.queryState('italic') == 1) {
+                            nodes.forEach(function(n) {
+                                n.setData('font-style').render();
+                            });
+                        } else {
+                            nodes.forEach(function(n) {
+                                n.setData('font-style', 'italic').render();
+                            });
+                        }
+
+                        km.layout();
+                    },
+                    queryState: function() {
+                        var nodes = km.getSelectedNodes(),
+                            result = 0;
+                        if (nodes.length === 0) {
+                            return -1;
+                        }
+                        nodes.forEach(function(n) {
+                            if (n && n.getData('font-style')) {
+                                result = 1;
+                                return false;
+                            }
+                        });
+                        return result;
+                    }
+                })
+            },
+            commandShortcutKeys: {
+                'bold': 'ctrl+b', //bold
+                'italic': 'ctrl+i' //italic
+            }
+        };
+    });
+});

+ 172 - 0
kityminder-editor/src/module/clipboard.js

@@ -0,0 +1,172 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+
+    Module.register('ClipboardModule', function() {
+        var km = this,
+            _clipboardNodes = [],
+            _selectedNodes = [];
+        
+        function appendChildNode(parent, child) {
+            _selectedNodes.push(child);
+            km.appendNode(child, parent);
+            child.render();
+            child.setLayoutOffset(null);
+            var children = child.children.map(function(node) {
+                return node.clone();
+            });
+
+            /*
+            * fixed bug: Modified on 2015.08.05
+            * 原因:粘贴递归 append 时没有清空原来父节点的子节点,而父节点被复制的时候,是连同子节点一起复制过来的
+            * 解决办法:增加了下面这一行代码
+            * by: @zhangbobell zhangbobell@163.com
+            */
+            child.clearChildren();
+
+            for (var i = 0, ci;
+                (ci = children[i]); i++) {
+                appendChildNode(child, ci);
+            }
+        }
+
+        function sendToClipboard(nodes) {
+            if (!nodes.length) return;
+            nodes.sort(function(a, b) {
+                return a.getIndex() - b.getIndex();
+            });
+            _clipboardNodes = nodes.map(function(node) {
+                return node.clone();
+            });
+        } 
+
+        /**
+         * @command Copy
+         * @description 复制当前选中的节点
+         * @shortcut Ctrl + C
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+        var CopyCommand = kity.createClass('CopyCommand', {
+            base: Command,
+
+            execute: function(km) {
+                sendToClipboard(km.getSelectedAncestors(true));
+                this.setContentChanged(false);
+            }
+        });
+
+        /**
+         * @command Cut
+         * @description 剪切当前选中的节点
+         * @shortcut Ctrl + X
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+        var CutCommand = kity.createClass('CutCommand', {
+            base: Command,
+
+            execute: function(km) {
+                var ancestors = km.getSelectedAncestors();
+
+                if (ancestors.length === 0) return;
+
+                sendToClipboard(ancestors);
+
+                km.select(MinderNode.getCommonAncestor(ancestors), true);
+
+                ancestors.slice().forEach(function(node) {
+                    km.removeNode(node);
+                });
+
+                km.layout(300);
+            }
+        });
+
+        /**
+         * @command Paste
+         * @description 粘贴已复制的节点到每一个当前选中的节点上
+         * @shortcut Ctrl + V
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+        var PasteCommand = kity.createClass('PasteCommand', {
+            base: Command,
+
+            execute: function(km) {
+                if (_clipboardNodes.length) {
+                    var nodes = km.getSelectedNodes();
+                    if (!nodes.length) return;
+
+                    for (var i = 0, ni; ni = _clipboardNodes[i]; i++) {
+                        for (var j = 0, node; node = nodes[j]; j++) {
+                            appendChildNode(node, ni.clone());
+                        }
+                    }
+
+                    km.select(_selectedNodes, true);
+                    _selectedNodes = [];
+
+                    km.layout(300);
+                }
+            },
+
+            queryState: function(km) {
+                return km.getSelectedNode() ? 0 : -1;
+            }
+        });
+
+        /**
+         * @Desc: 若支持原生clipboadr事件则基于原生扩展,否则使用km的基础事件只处理节点的粘贴复制
+         * @Editor: Naixor
+         * @Date: 2015.9.20
+         */
+        if (km.supportClipboardEvent && !kity.Browser.gecko) {
+            var Copy = function (e) {
+                this.fire('beforeCopy', e);
+            }
+
+            var Cut = function (e) {    
+                this.fire('beforeCut', e);
+            }
+
+            var Paste = function (e) {
+                this.fire('beforePaste', e);
+            }
+
+            return {
+                'commands': {
+                    'copy': CopyCommand,
+                    'cut': CutCommand,
+                    'paste': PasteCommand
+                },
+                'clipBoardEvents': {
+                    'copy': Copy.bind(km),
+                    'cut': Cut.bind(km),
+                    'paste': Paste.bind(km)
+                },
+                sendToClipboard: sendToClipboard
+            };
+        } else {
+            return {
+                'commands': {
+                    'copy': CopyCommand,
+                    'cut': CutCommand,
+                    'paste': PasteCommand
+                },
+                'commandShortcutKeys': {
+                    'copy': 'normal::ctrl+c|',
+                    'cut': 'normal::ctrl+x',
+                    'paste': 'normal::ctrl+v'
+                },
+                sendToClipboard: sendToClipboard
+            };
+        }
+    });
+});

+ 405 - 0
kityminder-editor/src/module/dragtree.js

@@ -0,0 +1,405 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+
+    // 矩形的变形动画定义
+    var MoveToParentCommand = kity.createClass('MoveToParentCommand', {
+        base: Command,
+        execute: function(minder, nodes, parent) {
+            var node;
+            for (var i = 0; i < nodes.length; i++) {
+                node = nodes[i];
+                if (node.parent) {
+                    node.parent.removeChild(node);
+                    parent.appendChild(node);
+                    node.render();
+                }
+            }
+            parent.expand();
+            minder.select(nodes, true);
+        }
+    });
+
+    var DropHinter = kity.createClass('DropHinter', {
+        base: kity.Group,
+
+        constructor: function() {
+            this.callBase();
+            this.rect = new kity.Rect();
+            this.addShape(this.rect);
+        },
+
+        render: function(target) {
+            this.setVisible(!!target);
+            if (target) {
+                this.rect
+                    .setBox(target.getLayoutBox())
+                    .setRadius(target.getStyle('radius') || 0)
+                    .stroke(
+                        target.getStyle('drop-hint-color') || 'yellow',
+                        target.getStyle('drop-hint-width') || 2
+                );
+                this.bringTop();
+            }
+        }
+    });
+
+    var OrderHinter = kity.createClass('OrderHinter', {
+        base: kity.Group,
+
+        constructor: function() {
+            this.callBase();
+            this.area = new kity.Rect();
+            this.path = new kity.Path();
+            this.addShapes([this.area, this.path]);
+        },
+
+        render: function(hint) {
+            this.setVisible(!!hint);
+            if (hint) {
+                this.area.setBox(hint.area);
+                this.area.fill(hint.node.getStyle('order-hint-area-color') || 'rgba(0, 255, 0, .5)');
+                this.path.setPathData(hint.path);
+                this.path.stroke(
+                    hint.node.getStyle('order-hint-path-color') || '#0f0',
+                    hint.node.getStyle('order-hint-path-width') || 1);
+            }
+        }
+    });
+
+    // 对拖动对象的一个替代盒子,控制整个拖放的逻辑,包括:
+    //    1. 从节点列表计算出拖动部分
+    //    2. 计算可以 drop 的节点,产生 drop 交互提示
+    var TreeDragger = kity.createClass('TreeDragger', {
+
+        constructor: function(minder) {
+            this._minder = minder;
+            this._dropHinter = new DropHinter();
+            this._orderHinter = new OrderHinter();
+            minder.getRenderContainer().addShapes([this._dropHinter, this._orderHinter]);
+        },
+
+        dragStart: function(position) {
+            // 只记录开始位置,不马上开启拖放模式
+            // 这个位置同时是拖放范围收缩时的焦点位置(中心)
+            this._startPosition = position;
+        },
+
+        dragMove: function(position) {
+            // 启动拖放模式需要最小的移动距离
+            var DRAG_MOVE_THRESHOLD = 10;
+
+            if (!this._startPosition) return;
+
+            var movement = kity.Vector.fromPoints(this._dragPosition || this._startPosition, position);
+            var minder = this._minder;
+
+            this._dragPosition = position;
+
+            if (!this._dragMode) {
+                // 判断拖放模式是否该启动
+                if (kity.Vector.fromPoints(this._dragPosition, this._startPosition).length() < DRAG_MOVE_THRESHOLD) {
+                    return;
+                }
+                if (!this._enterDragMode()) {
+                    return;
+                }
+            }
+
+            for (var i = 0; i < this._dragSources.length; i++) {
+                this._dragSources[i].setLayoutOffset(this._dragSources[i].getLayoutOffset().offset(movement));
+                minder.applyLayoutResult(this._dragSources[i]);
+            }
+
+            if (!this._dropTest()) {
+                this._orderTest();
+            } else {
+                this._renderOrderHint(this._orderSucceedHint = null);
+            }
+        },
+
+        dragEnd: function() {
+            this._startPosition = null;
+            this._dragPosition = null;
+
+            if (!this._dragMode) {
+                return;
+            }
+
+            this._fadeDragSources(1);
+
+            if (this._dropSucceedTarget) {
+
+                this._dragSources.forEach(function(source) {
+                    source.setLayoutOffset(null);
+                });
+
+                this._minder.layout(-1);
+
+                this._minder.execCommand('movetoparent', this._dragSources, this._dropSucceedTarget);
+
+            } else if (this._orderSucceedHint) {
+
+                var hint = this._orderSucceedHint;
+                var index = hint.node.getIndex();
+
+                var sourceIndexes = this._dragSources.map(function(source) {
+                    // 顺便干掉布局偏移
+                    source.setLayoutOffset(null);
+                    return source.getIndex();
+                });
+
+                var maxIndex = Math.max.apply(Math, sourceIndexes);
+                var minIndex = Math.min.apply(Math, sourceIndexes);
+
+                if (index < minIndex && hint.type == 'down') index++;
+                if (index > maxIndex && hint.type == 'up') index--;
+
+                hint.node.setLayoutOffset(null);
+
+                this._minder.execCommand('arrange', index);
+                this._renderOrderHint(null);
+            } else {
+                this._minder.fire('savescene');
+            }
+            this._minder.layout(300);
+            this._leaveDragMode();
+            this._minder.fire('contentchange');
+        },
+
+        // 进入拖放模式:
+        //    1. 计算拖放源和允许的拖放目标
+        //    2. 标记已启动
+        _enterDragMode: function() {
+            this._calcDragSources();
+            if (!this._dragSources.length) {
+                this._startPosition = null;
+                return false;
+            }
+            this._fadeDragSources(0.5);
+            this._calcDropTargets();
+            this._calcOrderHints();
+            this._dragMode = true;
+            this._minder.setStatus('dragtree');
+            return true;
+        },
+
+        // 从选中的节点计算拖放源
+        //    并不是所有选中的节点都作为拖放源,如果选中节点中存在 A 和 B,
+        //    并且 A 是 B 的祖先,则 B 不作为拖放源
+        //
+        //    计算过程:
+        //       1. 将节点按照树高排序,排序后只可能是前面节点是后面节点的祖先
+        //       2. 从后往前枚举排序的结果,如果发现枚举目标之前存在其祖先,
+        //          则排除枚举目标作为拖放源,否则加入拖放源
+        _calcDragSources: function() {
+            this._dragSources = this._minder.getSelectedAncestors();
+        },
+
+        _fadeDragSources: function(opacity) {
+            var minder = this._minder;
+            this._dragSources.forEach(function(source) {
+                source.getRenderContainer().setOpacity(opacity, 200);
+                source.traverse(function(node) {
+                    if (opacity < 1) {
+                        minder.detachNode(node);
+                    } else {
+                        minder.attachNode(node);
+                    }
+                }, true);
+            });
+        },
+
+
+        // 计算拖放目标可以释放的节点列表(释放意味着成为其子树),存在这条限制规则:
+        //    - 不能拖放到拖放目标的子树上(允许拖放到自身,因为多选的情况下可以把其它节点加入)
+        //
+        //    1. 加入当前节点(初始为根节点)到允许列表
+        //    2. 对于当前节点的每一个子节点:
+        //       (1) 如果是拖放目标的其中一个节点,忽略(整棵子树被剪枝)
+        //       (2) 如果不是拖放目标之一,以当前子节点为当前节点,回到 1 计算
+        //    3. 返回允许列表
+        //
+        _calcDropTargets: function() {
+
+            function findAvailableParents(nodes, root) {
+                var availables = [],
+                    i;
+                availables.push(root);
+                root.getChildren().forEach(function(test) {
+                    for (i = 0; i < nodes.length; i++) {
+                        if (nodes[i] == test) return;
+                    }
+                    availables = availables.concat(findAvailableParents(nodes, test));
+                });
+                return availables;
+            }
+
+            this._dropTargets = findAvailableParents(this._dragSources, this._minder.getRoot());
+            this._dropTargetBoxes = this._dropTargets.map(function(source) {
+                return source.getLayoutBox();
+            });
+        },
+
+        _calcOrderHints: function() {
+            var sources = this._dragSources;
+            var ancestor = MinderNode.getCommonAncestor(sources);
+
+            // 只有一个元素选中,公共祖先是其父
+            if (ancestor == sources[0]) ancestor = sources[0].parent;
+
+            if (sources.length === 0 || ancestor != sources[0].parent) {
+                this._orderHints = [];
+                return;
+            }
+
+            var siblings = ancestor.children;
+
+            this._orderHints = siblings.reduce(function(hint, sibling) {
+                if (sources.indexOf(sibling) == -1) {
+                    hint = hint.concat(sibling.getOrderHint());
+                }
+                return hint;
+            }, []);
+        },
+
+        _leaveDragMode: function() {
+            this._dragMode = false;
+            this._dropSucceedTarget = null;
+            this._orderSucceedHint = null;
+            this._renderDropHint(null);
+            this._renderOrderHint(null);
+            this._minder.rollbackStatus();
+        },
+
+        _drawForDragMode: function() {
+            this._text.setContent(this._dragSources.length + ' items');
+            this._text.setPosition(this._startPosition.x, this._startPosition.y + 5);
+            this._minder.getRenderContainer().addShape(this);
+        },
+
+        /**
+         * 通过 judge 函数判断 targetBox 和 sourceBox 的位置交叉关系
+         * @param targets -- 目标节点
+         * @param targetBoxMapper -- 目标节点与对应 Box 的映射关系
+         * @param judge -- 判断函数
+         * @returns {*}
+         * @private
+         */
+        _boxTest: function(targets, targetBoxMapper, judge) {
+            var sourceBoxes = this._dragSources.map(function(source) {
+                return source.getLayoutBox();
+            });
+
+            var i, j, target, sourceBox, targetBox;
+
+            judge = judge || function(intersectBox, sourceBox, targetBox) {
+                return intersectBox && !intersectBox.isEmpty();
+            };
+
+            for (i = 0; i < targets.length; i++) {
+
+                target = targets[i];
+                targetBox = targetBoxMapper.call(this, target, i);
+
+                for (j = 0; j < sourceBoxes.length; j++) {
+                    sourceBox = sourceBoxes[j];
+
+                    var intersectBox = sourceBox.intersect(targetBox);
+                    if (judge(intersectBox, sourceBox, targetBox)) {
+                        return target;
+                    }
+                }
+            }
+
+            return null;
+        },
+
+        _dropTest: function() {
+            this._dropSucceedTarget = this._boxTest(this._dropTargets, function(target, i) {
+                return this._dropTargetBoxes[i];
+            }, function(intersectBox, sourceBox, targetBox) {
+                function area(box) {
+                    return box.width * box.height;
+                }
+                if (!intersectBox) return false;
+                /*
+                * Added by zhangbobell, 2015.9.8
+                *
+                * 增加了下面一行判断,修复了循环比较中 targetBox 为折叠节点时,intersetBox 面积为 0,
+                * 而 targetBox 的 width 和 height 均为 0
+                * 此时造成了满足以下的第二个条件而返回 true
+                * */
+                if (!area(intersectBox)) return false;
+                // 面积判断,交叉面积大于其中的一半
+                if (area(intersectBox) > 0.5 * Math.min(area(sourceBox), area(targetBox))) return true;
+                // 有一个边完全重合的情况,也认为两个是交叉的
+                if (intersectBox.width + 1 >= Math.min(sourceBox.width, targetBox.width)) return true;
+                if (intersectBox.height + 1 >= Math.min(sourceBox.height, targetBox.height)) return true;
+                return false;
+            });
+            this._renderDropHint(this._dropSucceedTarget);
+            return !!this._dropSucceedTarget;
+        },
+
+        _orderTest: function() {
+            this._orderSucceedHint = this._boxTest(this._orderHints, function(hint) {
+                return hint.area;
+            });
+            this._renderOrderHint(this._orderSucceedHint);
+            return !!this._orderSucceedHint;
+        },
+
+        _renderDropHint: function(target) {
+            this._dropHinter.render(target);
+        },
+
+        _renderOrderHint: function(hint) {
+            this._orderHinter.render(hint);
+        },
+        preventDragMove: function() {
+            this._startPosition = null;
+        }
+    });
+
+    Module.register('DragTree', function() {
+        var dragger;
+
+        return {
+            init: function() {
+                dragger = new TreeDragger(this);
+                window.addEventListener('mouseup', function() {
+                    dragger.dragEnd();
+                });
+            },
+            events: {
+                'normal.mousedown inputready.mousedown': function(e) {
+                    // 单选中根节点也不触发拖拽
+                    if (e.originEvent.button) return;
+                    if (e.getTargetNode() && e.getTargetNode() != this.getRoot()) {
+                        dragger.dragStart(e.getPosition());
+                    }
+                },
+                'normal.mousemove dragtree.mousemove': function(e) {
+                    dragger.dragMove(e.getPosition());
+                },
+                'normal.mouseup dragtree.beforemouseup': function(e) {
+                    dragger.dragEnd();
+                    //e.stopPropagation();
+                    e.preventDefault();
+                },
+                'statuschange': function(e) {
+                    if (e.lastStatus == 'textedit' && e.currentStatus == 'normal') {
+                        dragger.preventDragMove();
+                    }
+                }
+            },
+            commands: {
+                'movetoparent': MoveToParentCommand
+            }
+        };
+    });
+});

+ 293 - 0
kityminder-editor/src/module/expand.js

@@ -0,0 +1,293 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+    var keymap = require('../core/keymap');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('Expand', function() {
+        var minder = this;
+        var EXPAND_STATE_DATA = 'expandState',
+            STATE_EXPAND = 'expand',
+            STATE_COLLAPSE = 'collapse';
+
+        // 将展开的操作和状态读取接口拓展到 MinderNode 上
+        kity.extendClass(MinderNode, {
+
+            /**
+             * 展开节点
+             * @param  {Policy} policy 展开的策略,默认为 KEEP_STATE
+             */
+            expand: function() {
+                this.setData(EXPAND_STATE_DATA, STATE_EXPAND);
+                return this;
+            },
+
+            /**
+             * 收起节点
+             */
+            collapse: function() {
+                this.setData(EXPAND_STATE_DATA, STATE_COLLAPSE);
+                return this;
+            },
+
+            /**
+             * 判断节点当前的状态是否为展开
+             */
+            isExpanded: function() {
+                var expanded = this.getData(EXPAND_STATE_DATA) !== STATE_COLLAPSE;
+                return expanded && (this.isRoot() || this.parent.isExpanded());
+            },
+
+            /**
+             * 判断节点当前的状态是否为收起
+             */
+            isCollapsed: function() {
+                return !this.isExpanded();
+            }
+        });
+
+        /**
+         * @command Expand
+         * @description 展开当前选中的节点,保证其可见
+         * @param {bool} justParents 是否只展开到父亲
+         *     * `false` - (默认)保证选中的节点以及其子树可见
+         *     * `true` - 只保证选中的节点可见,不展开其子树
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+        var ExpandCommand = kity.createClass('ExpandCommand', {
+            base: Command,
+
+            execute: function(km, justParents) {
+                var node = km.getSelectedNode();
+                if (!node) return;
+                if (justParents) {
+                    node = node.parent;
+                }
+                while (node.parent) {
+                    node.expand();
+                    node = node.parent;
+                }
+                node.renderTree();
+                km.layout(100);
+            },
+
+            queryState: function(km) {
+                var node = km.getSelectedNode();
+                return node && !node.isRoot() && !node.isExpanded() ? 0 : -1;
+            }
+        });
+
+        /**
+         * @command ExpandToLevel
+         * @description 展开脑图到指定的层级
+         * @param {number} level 指定展开到的层级,最少值为 1。
+         * @state
+         *   0: 一直可用
+         */
+        var ExpandToLevelCommand = kity.createClass('ExpandToLevelCommand', {
+            base: Command,
+            execute: function(km, level) {
+                km.getRoot().traverse(function(node) {
+                    if (node.getLevel() < level) node.expand();
+                    if (node.getLevel() == level && !node.isLeaf()) node.collapse();
+                });
+                km.refresh(100);
+            },
+            enableReadOnly: true
+        });
+
+        /**
+         * @command Collapse
+         * @description 收起当前节点的子树
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         */
+        var CollapseCommand = kity.createClass('CollapseCommand', {
+            base: Command,
+
+            execute: function(km) {
+                var node = km.getSelectedNode();
+                if (!node) return;
+
+                node.collapse();
+                node.renderTree();
+                km.layout();
+            },
+
+            queryState: function(km) {
+                var node = km.getSelectedNode();
+                return node && !node.isRoot() && node.isExpanded() ? 0 : -1;
+            }
+        });
+
+        var Expander = kity.createClass('Expander', {
+            base: kity.Group,
+
+            constructor: function(node) {
+                this.callBase();
+                this.radius = 6;
+                this.outline = new kity.Circle(this.radius).stroke('gray').fill('white');
+                this.sign = new kity.Path().stroke('gray');
+                this.addShapes([this.outline, this.sign]);
+                this.initEvent(node);
+                this.setId(utils.uuid('node_expander'));
+                this.setStyle('cursor', 'pointer');
+            },
+
+            initEvent: function(node) {
+                this.on('mousedown', function(e) {
+                    minder.select([node], true);
+                    if (node.isExpanded()) {
+                        node.collapse();
+                    } else {
+                        node.expand();
+                    }
+                    node.renderTree().getMinder().layout(100);
+                    node.getMinder().fire('contentchange');
+                    e.stopPropagation();
+                    e.preventDefault();
+                });
+                this.on('dblclick click mouseup', function(e) {
+                    e.stopPropagation();
+                    e.preventDefault();
+                });
+            },
+
+            setState: function(state) {
+                if (state == 'hide') {
+                    this.setVisible(false);
+                    return;
+                }
+                this.setVisible(true);
+                var pathData = ['M', 1.5 - this.radius, 0, 'L', this.radius - 1.5, 0];
+                if (state == STATE_COLLAPSE) {
+                    pathData.push(['M', 0, 1.5 - this.radius, 'L', 0, this.radius - 1.5]);
+                }
+                this.sign.setPathData(pathData);
+            }
+        });
+
+        var ExpanderRenderer = kity.createClass('ExpanderRenderer', {
+            base: Renderer,
+
+            create: function(node) {
+                if (node.isRoot()) return;
+                this.expander = new Expander(node);
+                node.getRenderContainer().prependShape(this.expander);
+                node.expanderRenderer = this;
+                this.node = node;
+                return this.expander;
+            },
+
+            shouldRender: function(node) {
+                return !node.isRoot();
+            },
+
+            update: function(expander, node, box) {
+                if (!node.parent) return;
+
+                var visible = node.parent.isExpanded();
+
+                expander.setState(visible && node.children.length ? node.getData(EXPAND_STATE_DATA) : 'hide');
+
+                var vector = node.getLayoutVectorIn().normalize(expander.radius + node.getStyle('stroke-width'));
+                var position = node.getVertexIn().offset(vector.reverse());
+
+                this.expander.setTranslate(position);
+            }
+        });
+
+        return {
+            commands: {
+                'expand': ExpandCommand,
+                'expandtolevel': ExpandToLevelCommand,
+                'collapse': CollapseCommand
+            },
+            events: {
+                'layoutapply': function(e) {
+                    var r = e.node.getRenderer('ExpanderRenderer');
+                    if (r.getRenderShape()) {
+                        r.update(r.getRenderShape(), e.node);
+                    }
+                },
+                'beforerender': function(e) {
+                    var node = e.node;
+                    var visible = !node.parent || node.parent.isExpanded();
+                    var minder = this;
+
+                    node.getRenderContainer().setVisible(visible);
+                    if (!visible) e.stopPropagation();
+                },
+                'normal.keydown': function(e) {
+                    if (this.getStatus() == 'textedit') return;
+                    if (e.originEvent.keyCode == keymap['/']) {
+                        var node = this.getSelectedNode();
+                        if (!node || node == this.getRoot()) return;
+                        var expanded = node.isExpanded();
+                        this.getSelectedNodes().forEach(function(node) {
+                            if (expanded) node.collapse();
+                            else node.expand();
+                            node.renderTree();
+                        });
+                        this.layout(100);
+                        this.fire('contentchange');
+                        e.preventDefault();
+                        e.stopPropagationImmediately();
+                    }
+                    if (e.isShortcutKey('Alt+`')) {
+                        this.execCommand('expandtolevel', 9999);
+                    }
+                    for (var i = 1; i < 6; i++) {
+                        if (e.isShortcutKey('Alt+' + i)) {
+                            this.execCommand('expandtolevel', i);
+                        }
+                    }
+                }
+            },
+            renderers: {
+                outside: ExpanderRenderer
+            },
+            contextmenu: [{
+                command: 'expandtoleaf',
+                query: function() {
+                    return !minder.getSelectedNode();
+                },
+                fn: function(minder) {
+                    minder.execCommand('expandtolevel', 9999);
+                }
+            }, {
+                command: 'expandtolevel1',
+                query: function() {
+                    return !minder.getSelectedNode();
+                },
+                fn: function(minder) {
+                    minder.execCommand('expandtolevel', 1);
+                }
+            }, {
+                command: 'expandtolevel2',
+                query: function() {
+                    return !minder.getSelectedNode();
+                },
+                fn: function(minder) {
+                    minder.execCommand('expandtolevel', 2);
+                }
+            },{
+                command: 'expandtolevel3',
+                query: function() {
+                    return !minder.getSelectedNode();
+                },
+                fn: function(minder) {
+                    minder.execCommand('expandtolevel', 3);
+                }
+            }, {
+                divider: true
+            }]
+        };
+    });
+});

+ 159 - 0
kityminder-editor/src/module/font.js

@@ -0,0 +1,159 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+
+    var TextRenderer = require('./text');
+
+    function getNodeDataOrStyle(node, name) {
+        return node.getData(name) || node.getStyle(name);
+    }
+
+    TextRenderer.registerStyleHook(function(node, textGroup) {
+        var dataColor = node.getData('color');
+        var selectedColor = node.getStyle('selected-color');
+        var styleColor = node.getStyle('color');
+
+        var foreColor = dataColor || (node.isSelected() && selectedColor ? selectedColor : styleColor);
+        var fontFamily = getNodeDataOrStyle(node, 'font-family');
+        var fontSize = getNodeDataOrStyle(node, 'font-size');
+
+        textGroup.fill(foreColor);
+
+        textGroup.eachItem(function(index, item) {
+            item.setFont({
+                'family': fontFamily,
+                'size': fontSize
+            });
+        });
+    });
+
+
+    Module.register('fontmodule', {
+        'commands': {
+            /**
+             * @command ForeColor
+             * @description 设置选中节点的字体颜色
+             * @param {string} color 表示颜色的字符串
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 如果只有一个节点选中,返回已选中节点的字体颜色;否则返回 'mixed'。
+             */
+            'forecolor': kity.createClass('fontcolorCommand', {
+                base: Command,
+                execute: function(km, color) {
+                    var nodes = km.getSelectedNodes();
+                    nodes.forEach(function(n) {
+                        n.setData('color', color);
+                        n.render();
+                    });
+                },
+                queryState: function(km) {
+                    return km.getSelectedNodes().length === 0 ? -1 : 0;
+                },
+                queryValue: function(km) {
+                    if (km.getSelectedNodes().length == 1) {
+                        return km.getSelectedNodes()[0].getData('color');
+                    }
+                    return 'mixed';
+                }
+            }),
+
+            /**
+             * @command Background
+             * @description 设置选中节点的背景颜色
+             * @param {string} color 表示颜色的字符串
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 如果只有一个节点选中,返回已选中节点的背景颜色;否则返回 'mixed'。
+             */
+            'background': kity.createClass('backgroudCommand', {
+                base: Command,
+
+                execute: function(km, color) {
+                    var nodes = km.getSelectedNodes();
+                    nodes.forEach(function(n) {
+                        n.setData('background', color);
+                        n.render();
+                    });
+                },
+                queryState: function(km) {
+                    return km.getSelectedNodes().length === 0 ? -1 : 0;
+                },
+                queryValue: function(km) {
+                    if (km.getSelectedNodes().length == 1) {
+                        return km.getSelectedNodes()[0].getData('background');
+                    }
+                    return 'mixed';
+                }
+            }),
+
+
+            /**
+             * @command FontFamily
+             * @description 设置选中节点的字体
+             * @param {string} family 表示字体的字符串
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 返回首个选中节点的字体
+             */
+            'fontfamily': kity.createClass('fontfamilyCommand', {
+                base: Command,
+
+                execute: function(km, family) {
+                    var nodes = km.getSelectedNodes();
+                    nodes.forEach(function(n) {
+                        n.setData('font-family', family);
+                        n.render();
+                        km.layout();
+                    });
+                },
+                queryState: function(km) {
+                    return km.getSelectedNodes().length === 0 ? -1 : 0;
+                },
+                queryValue: function(km) {
+                    var node = km.getSelectedNode();
+                    if (node) return node.getData('font-family');
+                    return null;
+                }
+            }),
+
+             /**
+             * @command FontSize
+             * @description 设置选中节点的字体大小
+             * @param {number} size 字体大小(px)
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 返回首个选中节点的字体大小
+             */
+            'fontsize': kity.createClass('fontsizeCommand', {
+                base: Command,
+
+                execute: function(km, size) {
+                    var nodes = km.getSelectedNodes();
+                    nodes.forEach(function(n) {
+                        n.setData('font-size', size);
+                        n.render();
+                        km.layout(300);
+                    });
+                },
+                queryState: function(km) {
+                    return km.getSelectedNodes().length === 0 ? -1 : 0;
+                },
+                queryValue: function(km) {
+                    var node = km.getSelectedNode();
+                    if (node) return node.getData('font-size');
+                    return null;
+                }
+            })
+        }
+    });
+});

+ 127 - 0
kityminder-editor/src/module/hyperlink.js

@@ -0,0 +1,127 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    // jscs:disable maximumLineLength
+    var linkShapePath = 'M16.614,10.224h-1.278c-1.668,0-3.07-1.07-3.599-2.556h4.877c0.707,0,1.278-0.571,1.278-1.278V3.834 c0-0.707-0.571-1.278-1.278-1.278h-4.877C12.266,1.071,13.668,0,15.336,0h1.278c2.116,0,3.834,1.716,3.834,3.834V6.39 C20.448,8.508,18.73,10.224,16.614,10.224z M5.112,5.112c0-0.707,0.573-1.278,1.278-1.278h7.668c0.707,0,1.278,0.571,1.278,1.278 S14.765,6.39,14.058,6.39H6.39C5.685,6.39,5.112,5.819,5.112,5.112z M2.556,3.834V6.39c0,0.707,0.573,1.278,1.278,1.278h4.877 c-0.528,1.486-1.932,2.556-3.599,2.556H3.834C1.716,10.224,0,8.508,0,6.39V3.834C0,1.716,1.716,0,3.834,0h1.278 c1.667,0,3.071,1.071,3.599,2.556H3.834C3.129,2.556,2.556,3.127,2.556,3.834z';
+
+    Module.register('hyperlink',{
+        'commands': {
+
+            /**
+             * @command HyperLink
+             * @description 为选中的节点添加超链接
+             * @param {string} url 超链接的 URL,设置为 null 移除
+             * @param {string} title 超链接的说明
+             * @state
+             *   0: 当前有选中的节点
+             *  -1: 当前没有选中的节点
+             * @return 返回首个选中节点的超链接信息,JSON 对象: `{url: url, title: title}`
+             */
+            'hyperlink': kity.createClass('hyperlink', {
+                base: Command,
+
+                execute: function(km, url, title) {
+                    var nodes = km.getSelectedNodes();
+                    nodes.forEach(function(n) {
+                        n.setData('hyperlink', url);
+                        n.setData('hyperlinkTitle', url && title);
+                        n.render();
+                    });
+                    km.layout();
+                },
+                queryState: function(km) {
+                    var nodes = km.getSelectedNodes(),
+                        result = 0;
+                    if (nodes.length === 0) {
+                        return -1;
+                    }
+                    nodes.forEach(function(n) {
+                        if (n && n.getData('hyperlink')) {
+                            result = 0;
+                            return false;
+                        }
+                    });
+                    return result;
+                },
+                queryValue: function(km) {
+                    var node = km.getSelectedNode();
+                    return {
+                        url: node.getData('hyperlink'),
+                        title: node.getData('hyperlinkTitle')
+                    };
+                }
+            })
+        },
+        'renderers': {
+            right: kity.createClass('hyperlinkrender', {
+                base: Renderer,
+
+                create: function() {
+
+                    var link = new kity.HyperLink();
+                    var linkshape = new kity.Path();
+                    var outline = new kity.Rect(24, 22, -2, -6, 4).fill('rgba(255, 255, 255, 0)');
+
+
+                    linkshape.setPathData(linkShapePath).fill('#666');
+                    link.addShape(outline);
+                    link.addShape(linkshape);
+                    link.setTarget('_blank');
+                    link.setStyle('cursor', 'pointer');
+
+                    link.on('mouseover', function() {
+                        outline.fill('rgba(255, 255, 200, .8)');
+                    }).on('mouseout', function() {
+                        outline.fill('rgba(255, 255, 255, 0)');
+                    });
+                    return link;
+                },
+
+                shouldRender: function(node) {
+                    return node.getData('hyperlink');
+                },
+
+                update: function(link, node, box) {
+
+                    var href = node.getData('hyperlink');
+                    link.setHref('#');
+
+                    var allowed = ['^http:', '^https:', '^ftp:', '^mailto:'];
+                    for (var i = 0; i < allowed.length; i++) {
+                        var regex = new RegExp(allowed[i]);
+                        if (regex.test(href)) {
+                            link.setHref(href);
+                            break;
+                        }
+                    }
+                    var title = node.getData('hyperlinkTitle');
+
+                    if (title) {
+                        title = [title, '(', href, ')'].join('');
+                    } else {
+                        title = href;
+                    }
+
+                    link.node.setAttributeNS('http://www.w3.org/1999/xlink', 'title', title);
+
+                    var spaceRight = node.getStyle('space-right');
+
+                    link.setTranslate(box.right + spaceRight + 2, -5);
+                    return new kity.Box({
+                        x: box.right + spaceRight,
+                        y: -11,
+                        width: 24,
+                        height: 22
+                    });
+                }
+            })
+        }
+    });
+});

+ 111 - 0
kityminder-editor/src/module/image-viewer.js

@@ -0,0 +1,111 @@
+define(function(require, exports, module) {
+
+    var kity = require('../core/kity');
+    var keymap = require('../core/keymap');
+
+    var Module = require('../core/module');
+    var Command = require('../core/command');
+
+    Module.register('ImageViewer', function() {
+
+        function createEl(name, classNames, children) {
+            var el = document.createElement(name);
+            addClass(el, classNames);
+            children && children.length && children.forEach(function (child) {
+                el.appendChild(child);
+            });
+            return el;
+        }
+
+        function on(el, event, handler) {
+            el.addEventListener(event, handler);
+        }
+
+        function addClass(el, classNames) {
+            classNames && classNames.split(' ').forEach(function (className) {
+                el.classList.add(className);
+            });
+        }
+
+        function removeClass(el, classNames) {
+            classNames && classNames.split(' ').forEach(function (className) {
+                el.classList.remove(className);
+            });
+        }
+
+        var ImageViewer = kity.createClass('ImageViewer', {
+            constructor: function () {
+                var btnClose = createEl('button', 'km-image-viewer-btn km-image-viewer-close');
+                var btnSource = createEl('button', 'km-image-viewer-btn km-image-viewer-source');
+                var image = this.image = createEl('img');
+                var toolbar = this.toolbar = createEl('div', 'km-image-viewer-toolbar', [btnSource, btnClose]);
+                var container = createEl('div', 'km-image-viewer-container', [image]);
+                var viewer = this.viewer = createEl('div', 'km-image-viewer', [toolbar, container]);
+                this.hotkeyHandler = this.hotkeyHandler.bind(this)
+                on(btnClose, 'click', this.close.bind(this));
+                on(btnSource, 'click', this.viewSource.bind(this));
+                on(image, 'click', this.zoomImage.bind(this));
+                on(viewer, 'contextmenu', this.toggleToolbar.bind(this));
+                on(document, 'keydown', this.hotkeyHandler);
+            },
+            dispose: function () {
+                this.close();
+                document.removeEventListener('remove', this.hotkeyHandler);
+            },
+            hotkeyHandler: function (e) {
+                if (!this.actived) {
+                    return;
+                }
+                if (e.keyCode === keymap['esc']) {
+                    this.close();
+                }
+            },
+            toggleToolbar: function (e) {
+                e && e.preventDefault();
+                this.toolbar.classList.toggle('hidden');
+            },
+            zoomImage: function (restore) {
+                var image = this.image;
+                if (typeof restore === 'boolean') {
+                    restore && addClass(image, 'limited');
+                }
+                else {
+                    image.classList.toggle('limited');
+                }
+            },
+            viewSource: function (src) {
+                window.open(this.image.src);
+            },
+            open: function (src) {
+                var input = document.querySelector('input');
+                if (input) {
+                    input.focus();
+                    input.blur();
+                }
+                this.image.src = src;
+                this.zoomImage(true);
+                document.body.appendChild(this.viewer);
+                this.actived = true;
+            },
+            close: function () {
+                this.image.src = '';
+                document.body.removeChild(this.viewer);
+                this.actived = false;
+            }
+        });
+
+        return {
+            init: function() {
+                this.viewer = new ImageViewer();
+            },
+            events: {
+                'normal.dblclick': function(e) {
+                    var shape = e.kityEvent.targetShape
+                    if (shape.__KityClassName === 'Image' && shape.url) {
+                        this.viewer.open(shape.url);
+                    }
+                }
+            }
+        };
+    });
+});

+ 147 - 0
kityminder-editor/src/module/image.js

@@ -0,0 +1,147 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('image', function() {
+        function loadImageSize(url, callback) {
+            var img = document.createElement('img');
+            img.onload = function() {
+                callback(img.width, img.height);
+            };
+            img.onerror = function() {
+                callback(null);
+            };
+            img.src = url;
+        }
+
+        function fitImageSize(width, height, maxWidth, maxHeight) {
+            var ratio = width / height,
+                fitRatio = maxWidth / maxHeight;
+
+            // 宽高比大于最大尺寸的宽高比,以宽度为标准适应
+            if (width > maxWidth && ratio > fitRatio) {
+                width = maxWidth;
+                height = width / ratio;
+            } else if (height > maxHeight) {
+                height = maxHeight;
+                width = height * ratio;
+            }
+
+            return {
+                width: width | 0,
+                height: height | 0
+            };
+        }
+
+        /**
+         * @command Image
+         * @description 为选中的节点添加图片
+         * @param {string} url 图片的 URL,设置为 null 移除
+         * @param {string} title 图片的说明
+         * @state
+         *   0: 当前有选中的节点
+         *  -1: 当前没有选中的节点
+         * @return 返回首个选中节点的图片信息,JSON 对象: `{url: url, title: title}`
+         */
+        var ImageCommand = kity.createClass('ImageCommand', {
+            base: Command,
+
+            execute: function(km, url, title) {
+                var nodes = km.getSelectedNodes();
+
+                loadImageSize(url, function(width, height) {
+                    nodes.forEach(function(n) {
+                        var size = fitImageSize(
+                            width, height,
+                            km.getOption('maxImageWidth'),
+                            km.getOption('maxImageHeight'));
+                        n.setData('image', url);
+                        n.setData('imageTitle', url && title);
+                        n.setData('imageSize', url && size);
+                        n.render();
+                    });
+                    km.fire('saveScene');
+                    km.layout(300);
+                });
+
+            },
+            queryState: function(km) {
+                var nodes = km.getSelectedNodes(),
+                    result = 0;
+                if (nodes.length === 0) {
+                    return -1;
+                }
+                nodes.forEach(function(n) {
+                    if (n && n.getData('image')) {
+                        result = 0;
+                        return false;
+                    }
+                });
+                return result;
+            },
+            queryValue: function(km) {
+                var node = km.getSelectedNode();
+                return {
+                    url: node.getData('image'),
+                    title: node.getData('imageTitle')
+                };
+            }
+        });
+
+        var ImageRenderer = kity.createClass('ImageRenderer', {
+            base: Renderer,
+
+            create: function(node) {
+                return new kity.Image(node.getData('image'));
+            },
+
+            shouldRender: function(node) {
+                return node.getData('image');
+            },
+
+            update: function(image, node, box) {
+                var url = node.getData('image');
+                var title = node.getData('imageTitle');
+                var size = node.getData('imageSize');
+                var spaceTop = node.getStyle('space-top');
+
+                if (!size) return;
+
+                if (title) {
+                    image.node.setAttributeNS('http://www.w3.org/1999/xlink', 'title', title);
+                }
+
+                var x = box.cx - size.width / 2;
+                var y = box.y - size.height - spaceTop;
+
+                image
+                    .setUrl(url)
+                    .setX(x | 0)
+                    .setY(y | 0)
+                    .setWidth(size.width | 0)
+                    .setHeight(size.height | 0);
+
+                return new kity.Box(x | 0, y | 0, size.width | 0, size.height | 0);
+            }
+        });
+
+        return {
+            'defaultOptions': {
+                'maxImageWidth': 200,
+                'maxImageHeight': 200
+            },
+            'commands': {
+                'image': ImageCommand
+            },
+            'renderers': {
+                'top': ImageRenderer
+            }
+        };
+    });
+});

+ 171 - 0
kityminder-editor/src/module/keynav.js

@@ -0,0 +1,171 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+    var keymap = require('../core/keymap');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('KeyboardModule', function() {
+        var min = Math.min,
+            max = Math.max,
+            abs = Math.abs,
+            sqrt = Math.sqrt,
+            exp = Math.exp;
+
+        function buildPositionNetwork(root) {
+            var pointIndexes = [],
+                p;
+            root.traverse(function(node) {
+                p = node.getLayoutBox();
+
+                // bugfix: 不应导航到收起的节点(判断其尺寸是否存在)
+                if (p.width && p.height) {
+                    pointIndexes.push({
+                        left: p.x,
+                        top: p.y,
+                        right: p.x + p.width,
+                        bottom: p.y + p.height,
+                        width: p.width,
+                        height: p.height,
+                        node: node
+                    });
+                }
+            });
+            for (var i = 0; i < pointIndexes.length; i++) {
+                findClosestPointsFor(pointIndexes, i);
+            }
+        }
+
+        // 这是金泉的点子,赞!
+        // 求两个不相交矩形的最近距离
+        function getCoefedDistance(box1, box2) {
+            var xMin, xMax, yMin, yMax, xDist, yDist, dist, cx, cy;
+            xMin = min(box1.left, box2.left);
+            xMax = max(box1.right, box2.right);
+            yMin = min(box1.top, box2.top);
+            yMax = max(box1.bottom, box2.bottom);
+
+            xDist = xMax - xMin - box1.width - box2.width;
+            yDist = yMax - yMin - box1.height - box2.height;
+
+            if (xDist < 0) dist = yDist;
+            else if (yDist < 0) dist = xDist;
+            else dist = sqrt(xDist * xDist + yDist * yDist);
+
+            var node1 = box1.node;
+            var node2 = box2.node;
+
+            // sibling
+            if (node1.parent == node2.parent) {
+                dist /= 10;
+            }
+            // parent
+            if (node2.parent == node1) {
+                dist /= 5;
+            }
+
+            return dist;
+        }
+
+        function findClosestPointsFor(pointIndexes, iFind) {
+            var find = pointIndexes[iFind];
+            var most = {},
+                quad;
+            var current, dist;
+
+            for (var i = 0; i < pointIndexes.length; i++) {
+
+                if (i == iFind) continue;
+                current = pointIndexes[i];
+
+                dist = getCoefedDistance(current, find);
+
+                // left check
+                if (current.right < find.left) {
+                    if (!most.left || dist < most.left.dist) {
+                        most.left = {
+                            dist: dist,
+                            node: current.node
+                        };
+                    }
+                }
+
+                // right check
+                if (current.left > find.right) {
+                    if (!most.right || dist < most.right.dist) {
+                        most.right = {
+                            dist: dist,
+                            node: current.node
+                        };
+                    }
+                }
+
+                // top check
+                if (current.bottom < find.top) {
+                    if (!most.top || dist < most.top.dist) {
+                        most.top = {
+                            dist: dist,
+                            node: current.node
+                        };
+                    }
+                }
+
+                // bottom check
+                if (current.top > find.bottom) {
+                    if (!most.down || dist < most.down.dist) {
+                        most.down = {
+                            dist: dist,
+                            node: current.node
+                        };
+                    }
+                }
+            }
+            find.node._nearestNodes = {
+                right: most.right && most.right.node || null,
+                top: most.top && most.top.node || null,
+                left: most.left && most.left.node || null,
+                down: most.down && most.down.node || null
+            };
+        }
+
+        function navigateTo(km, direction) {
+            var referNode = km.getSelectedNode();
+            if (!referNode) {
+                km.select(km.getRoot());
+                buildPositionNetwork(km.getRoot());
+                return;
+            }
+            if (!referNode._nearestNodes) {
+                buildPositionNetwork(km.getRoot());
+            }
+            var nextNode = referNode._nearestNodes[direction];
+            if (nextNode) {
+                km.select(nextNode, true);
+            }
+        }
+
+        // 稀释用
+        var lastFrame;
+        return {
+            'events': {
+                'layoutallfinish': function() {
+                    var root = this.getRoot();
+                    buildPositionNetwork(root);
+                },
+                'normal.keydown readonly.keydown': function(e) {
+                    var minder = this;
+                    ['left', 'right', 'up', 'down'].forEach(function(key) {
+                        if (e.isShortcutKey(key)) {
+                            navigateTo(minder, key == 'up' ? 'top' : key);
+                            e.preventDefault();
+                        }
+                    });
+                }
+            }
+        };
+    });
+});

+ 92 - 0
kityminder-editor/src/module/layout.js

@@ -0,0 +1,92 @@
+/**
+ * @fileOverview
+ *
+ * 布局模块
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+
+    /**
+     * @command Layout
+     * @description 设置选中节点的布局
+     *     允许使用的布局可以使用 `kityminder.Minder.getLayoutList()` 查询
+     * @param {string} name 布局的名称,设置为 null 则使用继承或默认的布局
+     * @state
+     *   0: 当前有选中的节点
+     *  -1: 当前没有选中的节点
+     * @return 返回首个选中节点的布局名称
+     */
+    var LayoutCommand = kity.createClass('LayoutCommand', {
+        base: Command,
+
+        execute: function(minder, name) {
+            var nodes = minder.getSelectedNodes();
+            nodes.forEach(function(node) {
+                node.layout(name);
+            });
+        },
+
+        queryValue: function(minder) {
+            var node = minder.getSelectedNode();
+            if (node) {
+                return node.getData('layout');
+            }
+        },
+
+        queryState: function(minder) {
+            return minder.getSelectedNode() ? 0 : -1;
+        }
+    });
+
+    /**
+     * @command ResetLayout
+     * @description 重设选中节点的布局,如果当前没有选中的节点,重设整个脑图的布局
+     * @state
+     *   0: 始终可用
+     * @return 返回首个选中节点的布局名称
+     */
+    var ResetLayoutCommand = kity.createClass('ResetLayoutCommand', {
+        base: Command,
+
+        execute: function(minder) {
+            var nodes = minder.getSelectedNodes();
+
+            if (!nodes.length) nodes = [minder.getRoot()];
+
+            nodes.forEach(function(node) {
+                node.traverse(function(child) {
+                    child.resetLayoutOffset();
+                    if (!child.isRoot()) {
+                        child.setData('layout', null);
+                    }
+                });
+            });
+            minder.layout(300);
+        },
+
+        enableReadOnly: true
+    });
+
+    Module.register('LayoutModule', {
+        commands: {
+            'layout': LayoutCommand,
+            'resetlayout': ResetLayoutCommand
+        },
+        contextmenu: [{
+            command: 'resetlayout'
+        }, {
+            divider: true
+        }],
+
+        commandShortcutKeys: {
+            'resetlayout': 'Ctrl+Shift+L'
+        }
+    });
+
+});

+ 150 - 0
kityminder-editor/src/module/node.js

@@ -0,0 +1,150 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    /**
+     * @command AppendChildNode
+     * @description 添加子节点到选中的节点中
+     * @param {string|object} textOrData 要插入的节点的文本或数据
+     * @state
+     *    0: 当前有选中的节点
+     *   -1: 当前没有选中的节点
+     */
+    var AppendChildCommand = kity.createClass('AppendChildCommand', {
+        base: Command,
+        execute: function(km, text) {
+            var parent = km.getSelectedNode();
+            if (!parent) {
+                return null;
+            }
+            var node = km.createNode(text, parent);
+            km.select(node, true);
+            if (parent.isExpanded()) {
+                node.render();
+            }
+            else {
+                parent.expand();
+                parent.renderTree();
+            }
+            km.layout(600);
+        },
+        queryState: function(km) {
+            var selectedNode = km.getSelectedNode();
+            return selectedNode ? 0 : -1;
+        }
+    });
+
+    /**
+     * @command AppendSiblingNode
+     * @description 添加选中的节点的兄弟节点
+     * @param {string|object} textOrData 要添加的节点的文本或数据
+     * @state
+     *    0: 当前有选中的节点
+     *   -1: 当前没有选中的节点
+     */
+    var AppendSiblingCommand = kity.createClass('AppendSiblingCommand', {
+        base: Command,
+        execute: function(km, text) {
+            var sibling = km.getSelectedNode();
+            var parent = sibling.parent;
+            if (!parent) {
+                return km.execCommand('AppendChildNode', text);
+            }
+            var node = km.createNode(text, parent, sibling.getIndex() + 1);
+            node.setGlobalLayoutTransform(sibling.getGlobalLayoutTransform());
+            km.select(node, true);
+            node.render();
+            km.layout(600);
+        },
+        queryState: function(km) {
+            var selectedNode = km.getSelectedNode();
+            return selectedNode ? 0 : -1;
+        }
+    });
+
+    /**
+     * @command RemoveNode
+     * @description 移除选中的节点
+     * @state
+     *    0: 当前有选中的节点
+     *   -1: 当前没有选中的节点
+     */
+    var RemoveNodeCommand = kity.createClass('RemoverNodeCommand', {
+        base: Command,
+        execute: function(km) {
+            var nodes = km.getSelectedNodes();
+            var ancestor = MinderNode.getCommonAncestor.apply(null, nodes);
+            var index = nodes[0].getIndex();
+
+            nodes.forEach(function(node) {
+                if (!node.isRoot()) km.removeNode(node);
+            });
+            if (nodes.length == 1) {
+                var selectBack = ancestor.children[index - 1] || ancestor.children[index];
+                km.select(selectBack || ancestor || km.getRoot(), true);
+            } else {
+                km.select(ancestor || km.getRoot(), true);
+            }
+            km.layout(600);
+        },
+        queryState: function(km) {
+            var selectedNode = km.getSelectedNode();
+            return selectedNode && !selectedNode.isRoot() ? 0 : -1;
+        }
+    });
+
+    var AppendParentCommand = kity.createClass('AppendParentCommand', {
+        base: Command,
+        execute: function(km, text) {
+            var nodes = km.getSelectedNodes();
+
+            nodes.sort(function(a, b) {
+                return a.getIndex() - b.getIndex();
+            });
+            var parent = nodes[0].parent;
+
+            var newParent = km.createNode(text, parent, nodes[0].getIndex());
+            nodes.forEach(function(node) {
+                newParent.appendChild(node);
+            });
+            newParent.setGlobalLayoutTransform(nodes[nodes.length >> 1].getGlobalLayoutTransform());
+
+            km.select(newParent, true);
+            km.layout(600);
+        },
+        queryState: function(km) {
+            var nodes = km.getSelectedNodes();
+            if (!nodes.length) return -1;
+            var parent = nodes[0].parent;
+            if (!parent) return -1;
+            for (var i = 1; i < nodes.length; i++) {
+                if (nodes[i].parent != parent) return -1;
+            }
+            return 0;
+        }
+    });
+
+    Module.register('NodeModule', function() {
+        return {
+            commands: {
+                'AppendChildNode': AppendChildCommand,
+                'AppendSiblingNode': AppendSiblingCommand,
+                'RemoveNode': RemoveNodeCommand,
+                'AppendParentNode': AppendParentCommand
+            },
+
+            'commandShortcutKeys': {
+                'appendsiblingnode': 'normal::Enter',
+                'appendchildnode': 'normal::Insert|Tab',
+                'appendparentnode': 'normal::Shift+Tab|normal::Shift+Insert',
+                'removenode': 'normal::Del|Backspace'
+            }
+        };
+    });
+});

+ 116 - 0
kityminder-editor/src/module/note.js

@@ -0,0 +1,116 @@
+/**
+ * @fileOverview
+ *
+ * 支持节点详细信息(HTML)格式
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('NoteModule', function() {
+
+        var NOTE_PATH = 'M9,9H3V8h6L9,9L9,9z M9,7H3V6h6V7z M9,5H3V4h6V5z M8.5,11H2V2h8v7.5 M9,12l2-2V1H1v11';
+
+
+        /**
+         * @command Note
+         * @description 设置节点的备注信息
+         * @param {string} note 要设置的备注信息,设置为 null 则移除备注信息
+         * @state
+         *    0: 当前有选中的节点
+         *   -1: 当前没有选中的节点
+         */
+        var NoteCommand = kity.createClass('NoteCommand', {
+            base: Command,
+
+            execute: function(minder, note) {
+                var node = minder.getSelectedNode();
+                node.setData('note', note);
+                node.render();
+                node.getMinder().layout(300);
+            },
+
+            queryState: function(minder) {
+                return minder.getSelectedNodes().length === 1 ? 0 : -1;
+            },
+
+            queryValue: function(minder) {
+                var node = minder.getSelectedNode();
+                return node && node.getData('note');
+            }
+        });
+
+        var NoteIcon = kity.createClass('NoteIcon', {
+            base: kity.Group,
+
+            constructor: function() {
+                this.callBase();
+                this.width = 16;
+                this.height = 17;
+                this.rect = new kity.Rect(16, 17, 0.5, -8.5, 2).fill('transparent');
+                this.path = new kity.Path().setPathData(NOTE_PATH).setTranslate(2.5, -6.5);
+                this.addShapes([this.rect, this.path]);
+
+                this.on('mouseover', function() {
+                    this.rect.fill('rgba(255, 255, 200, .8)');
+                }).on('mouseout', function() {
+                    this.rect.fill('transparent');
+                });
+
+                this.setStyle('cursor', 'pointer');
+            }
+        });
+
+        var NoteIconRenderer = kity.createClass('NoteIconRenderer', {
+            base: Renderer,
+
+            create: function(node) {
+                var icon = new NoteIcon();
+                icon.on('mousedown', function(e) {
+                    e.preventDefault();
+                    node.getMinder().fire('editnoterequest');
+                });
+                icon.on('mouseover', function() {
+                    node.getMinder().fire('shownoterequest', {node: node, icon: icon});
+                });
+                icon.on('mouseout', function() {
+                    node.getMinder().fire('hidenoterequest', {node: node, icon: icon});
+                });
+                return icon;
+            },
+
+            shouldRender: function(node) {
+                return node.getData('note');
+            },
+
+            update: function(icon, node, box) {
+                var x = box.right + node.getStyle('space-left');
+                var y = box.cy;
+
+                icon.path.fill(node.getStyle('color'));
+                icon.setTranslate(x, y);
+
+                return new kity.Box(x, Math.round(y - icon.height / 2), icon.width, icon.height);
+            }
+
+        });
+
+        return {
+            renderers: {
+                right: NoteIconRenderer
+            },
+            commands: {
+                'note': NoteCommand
+            }
+        };
+    });
+});

+ 165 - 0
kityminder-editor/src/module/outline.js

@@ -0,0 +1,165 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    var OutlineRenderer = kity.createClass('OutlineRenderer', {
+        base: Renderer,
+
+        create: function(node) {
+
+            var outline = new kity.Rect()
+                .setId(utils.uuid('node_outline'));
+
+            this.bringToBack = true;
+
+            return outline;
+        },
+
+        update: function(outline, node, box) {
+
+            var shape = node.getStyle('shape');
+
+            var paddingLeft = node.getStyle('padding-left'),
+                paddingRight = node.getStyle('padding-right'),
+                paddingTop = node.getStyle('padding-top'),
+                paddingBottom = node.getStyle('padding-bottom');
+
+            var outlineBox = {
+                x: box.x - paddingLeft,
+                y: box.y - paddingTop,
+                width: box.width + paddingLeft + paddingRight,
+                height: box.height + paddingTop + paddingBottom
+            };
+
+
+            var radius = node.getStyle('radius');
+            // 天盘图圆形的情况
+            if (shape && shape == 'circle') {
+                var p = Math.pow;
+                var r = Math.round;
+
+                radius = r(Math.sqrt(p(outlineBox.width, 2) + p(outlineBox.height, 2)) / 2);
+
+                outlineBox.x = box.cx - radius;
+                outlineBox.y = box.cy - radius;
+                outlineBox.width = 2 * radius;
+                outlineBox.height = 2 * radius;
+            }
+
+            var prefix = node.isSelected() ? (node.getMinder().isFocused() ? 'selected-' : 'blur-selected-') : '';
+            outline
+                .setPosition(outlineBox.x, outlineBox.y)
+                .setSize(outlineBox.width, outlineBox.height)
+                .setRadius(radius)
+                .fill(node.getData('background') || node.getStyle(prefix + 'background') || node.getStyle('background'))
+                .stroke(node.getStyle(prefix + 'stroke' || node.getStyle('stroke')),
+                node.getStyle(prefix + 'stroke-width'));
+
+            return new kity.Box(outlineBox);
+        }
+    });
+
+    var ShadowRenderer = kity.createClass('ShadowRenderer', {
+        base: Renderer,
+
+        create: function(node) {
+            this.bringToBack = true;
+            return new kity.Rect();
+        },
+
+        shouldRender: function(node) {
+            return node.getStyle('shadow');
+        },
+
+        update: function(shadow, node, box) {
+            shadow.setPosition(box.x + 4, box.y + 5)
+                .fill(node.getStyle('shadow'));
+
+            var shape = node.getStyle('shape');
+            if(!shape){
+                shadow.setSize(box.width, box.height);
+                shadow.setRadius(node.getStyle('radius'));
+
+            }else if(shape=='circle'){
+                var width= Math.max(box.width,box.height);
+                shadow.setSize(width, width);
+                shadow.setRadius(width/2);
+            }
+        }
+    });
+
+    var marker = new kity.Marker();
+
+    marker.setWidth(10);
+    marker.setHeight(12);
+    marker.setRef(0, 0);
+    marker.setViewBox(-6, -4, 8, 10);
+
+    marker.addShape(new kity.Path().setPathData('M-5-3l5,3,-5,3').stroke('#33ffff'));
+
+    var wireframeOption = /wire/.test(window.location.href);
+    var WireframeRenderer = kity.createClass('WireframeRenderer', {
+        base: Renderer,
+
+        create: function() {
+            var wireframe = new kity.Group();
+            var oxy = this.oxy = new kity.Path()
+                .stroke('#f6f')
+                .setPathData('M0,-50L0,50M-50,0L50,0');
+
+            var box = this.wireframe = new kity.Rect()
+                .stroke('lightgreen');
+
+            var vectorIn = this.vectorIn = new kity.Path()
+                .stroke('#66ffff');
+            var vectorOut = this.vectorOut = new kity.Path()
+                .stroke('#66ffff');
+
+            vectorIn.setMarker(marker, 'end');
+            vectorOut.setMarker(marker, 'end');
+
+            return wireframe.addShapes([oxy, box, vectorIn, vectorOut]);
+        },
+
+        shouldRender: function() {
+            return wireframeOption;
+        },
+
+        update: function(created, node, box) {
+            this.wireframe
+                .setPosition(box.x, box.y)
+                .setSize(box.width, box.height);
+            var pin = node.getVertexIn();
+            var pout = node.getVertexOut();
+            var vin = node.getLayoutVectorIn().normalize(30);
+            var vout = node.getLayoutVectorOut().normalize(30);
+            this.vectorIn.setPathData(['M', pin.offset(vin.reverse()), 'L', pin]);
+            this.vectorOut.setPathData(['M', pout, 'l', vout]);
+        }
+    });
+
+    Module.register('OutlineModule', function() {
+        return {
+            events: (!wireframeOption ? null : {
+                'ready': function() {
+                    this.getPaper().addResource(marker);
+                },
+                'layoutallfinish': function() {
+                    this.getRoot().traverse(function(node) {
+                        node.getRenderer('WireframeRenderer').update(null, node, node.getContentBox());
+                    });
+                }
+            }),
+            renderers: {
+                outline: OutlineRenderer,
+                outside: [ShadowRenderer, WireframeRenderer]
+            }
+        };
+    });
+});

+ 156 - 0
kityminder-editor/src/module/priority.js

@@ -0,0 +1,156 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('PriorityModule', function() {
+        var minder = this;
+
+        // Designed by Akikonata
+        // [MASK, BACK]
+        var PRIORITY_COLORS = [null, ['#FF1200', '#840023'], // 1 - red
+            ['#0074FF', '#01467F'], // 2 - blue
+            ['#00AF00', '#006300'], // 3 - green
+            ['#FF962E', '#B25000'], // 4 - orange
+            ['#A464FF', '#4720C4'], // 5 - purple
+            ['#A3A3A3', '#515151'], // 6,7,8,9 - gray
+            ['#A3A3A3', '#515151'],
+            ['#A3A3A3', '#515151'],
+            ['#A3A3A3', '#515151'],
+        ]; // hue from 1 to 5
+
+        // jscs:disable maximumLineLength
+        var BACK_PATH = 'M0,13c0,3.866,3.134,7,7,7h6c3.866,0,7-3.134,7-7V7H0V13z';
+        var MASK_PATH = 'M20,10c0,3.866-3.134,7-7,7H7c-3.866,0-7-3.134-7-7V7c0-3.866,3.134-7,7-7h6c3.866,0,7,3.134,7,7V10z';
+
+        var PRIORITY_DATA = 'priority';
+
+        // 优先级图标的图形
+        var PriorityIcon = kity.createClass('PriorityIcon', {
+            base: kity.Group,
+
+            constructor: function() {
+                this.callBase();
+                this.setSize(20);
+                this.create();
+                this.setId(utils.uuid('node_priority'));
+            },
+
+            setSize: function(size) {
+                this.width = this.height = size;
+            },
+
+            create: function() {
+                var white, back, mask, number; // 4 layer
+
+                white = new kity.Path().setPathData(MASK_PATH).fill('white');
+                back = new kity.Path().setPathData(BACK_PATH).setTranslate(0.5, 0.5);
+                mask = new kity.Path().setPathData(MASK_PATH).setOpacity(0.8).setTranslate(0.5, 0.5);
+
+                number = new kity.Text()
+                    .setX(this.width / 2 - 0.5).setY(this.height / 2)
+                    .setTextAnchor('middle')
+                    .setVerticalAlign('middle')
+                    .setFontItalic(true)
+                    .setFontSize(12)
+                    .fill('white');
+
+                this.addShapes([back, mask, number]);
+                this.mask = mask;
+                this.back = back;
+                this.number = number;
+            },
+
+            setValue: function(value) {
+                var back = this.back,
+                    mask = this.mask,
+                    number = this.number;
+
+                var color = PRIORITY_COLORS[value];
+
+                if (color) {
+                    back.fill(color[1]);
+                    mask.fill(color[0]);
+                }
+
+                number.setContent(value);
+            }
+        });
+
+        /**
+         * @command Priority
+         * @description 设置节点的优先级信息
+         * @param {number} value 要设置的优先级(添加一个优先级小图标)
+         *     取值为 0 移除优先级信息;
+         *     取值为 1 - 9 设置优先级,超过 9 的优先级不渲染
+         * @state
+         *    0: 当前有选中的节点
+         *   -1: 当前没有选中的节点
+         */
+        var PriorityCommand = kity.createClass('SetPriorityCommand', {
+            base: Command,
+            execute: function(km, value) {
+                var nodes = km.getSelectedNodes();
+                for (var i = 0; i < nodes.length; i++) {
+                    nodes[i].setData(PRIORITY_DATA, value || null).render();
+                }
+                km.layout();
+            },
+            queryValue: function(km) {
+                var nodes = km.getSelectedNodes();
+                var val;
+                for (var i = 0; i < nodes.length; i++) {
+                    val = nodes[i].getData(PRIORITY_DATA);
+                    if (val) break;
+                }
+                return val || null;
+            },
+
+            queryState: function(km) {
+                return km.getSelectedNodes().length ? 0 : -1;
+            }
+        });
+        return {
+            'commands': {
+                'priority': PriorityCommand
+            },
+            'renderers': {
+                left: kity.createClass('PriorityRenderer', {
+                    base: Renderer,
+
+                    create: function(node) {
+                        return new PriorityIcon();
+                    },
+
+                    shouldRender: function(node) {
+                        return node.getData(PRIORITY_DATA);
+                    },
+
+                    update: function(icon, node, box) {
+                        var data = node.getData(PRIORITY_DATA);
+                        var spaceLeft = node.getStyle('space-left'),
+                            x, y;
+
+                        icon.setValue(data);
+                        x = box.left - icon.width - spaceLeft;
+                        y = -icon.height / 2;
+
+                        icon.setTranslate(x, y);
+
+                        return new kity.Box({
+                            x: x,
+                            y: y,
+                            width: icon.width,
+                            height: icon.height
+                        });
+                    }
+                })
+            }
+        };
+    });
+});

+ 160 - 0
kityminder-editor/src/module/progress.js

@@ -0,0 +1,160 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('ProgressModule', function() {
+        var minder = this;
+
+        var PROGRESS_DATA = 'progress';
+
+        // Designed by Akikonata
+        var BG_COLOR = '#FFED83';
+        var PIE_COLOR = '#43BC00';
+        var SHADOW_PATH = 'M10,3c4.418,0,8,3.582,8,8h1c0-5.523-3.477-10-9-10S1,5.477,1,11h1C2,6.582,5.582,3,10,3z';
+        var SHADOW_COLOR = '#8E8E8E';
+
+        // jscs:disable maximumLineLength
+        var FRAME_PATH = 'M10,0C4.477,0,0,4.477,0,10c0,5.523,4.477,10,10,10s10-4.477,10-10C20,4.477,15.523,0,10,0zM10,18c-4.418,0-8-3.582-8-8s3.582-8,8-8s8,3.582,8,8S14.418,18,10,18z';
+
+        var FRAME_GRAD = new kity.LinearGradient().pipe(function(g) {
+            g.setStartPosition(0, 0);
+            g.setEndPosition(0, 1);
+            g.addStop(0, '#fff');
+            g.addStop(1, '#ccc');
+        });
+        var CHECK_PATH = 'M15.812,7.896l-6.75,6.75l-4.5-4.5L6.25,8.459l2.812,2.803l5.062-5.053L15.812,7.896z';
+        var CHECK_COLOR = '#EEE';
+
+        minder.getPaper().addResource(FRAME_GRAD);
+
+        // 进度图标的图形
+        var ProgressIcon = kity.createClass('ProgressIcon', {
+            base: kity.Group,
+
+            constructor: function(value) {
+                this.callBase();
+                this.setSize(20);
+                this.create();
+                this.setValue(value);
+                this.setId(utils.uuid('node_progress'));
+                this.translate(0.5, 0.5);
+            },
+
+            setSize: function(size) {
+                this.width = this.height = size;
+            },
+
+            create: function() {
+
+                var bg, pie, shadow, frame, check;
+
+                bg = new kity.Circle(9)
+                    .fill(BG_COLOR);
+
+                pie = new kity.Pie(9, 0)
+                    .fill(PIE_COLOR);
+
+                shadow = new kity.Path()
+                    .setPathData(SHADOW_PATH)
+                    .setTranslate(-10, -10)
+                    .fill(SHADOW_COLOR);
+
+                frame = new kity.Path()
+                    .setTranslate(-10, -10)
+                    .setPathData(FRAME_PATH)
+                    .fill(FRAME_GRAD);
+
+                check = new kity.Path()
+                    .setTranslate(-10, -10)
+                    .setPathData(CHECK_PATH)
+                    .fill(CHECK_COLOR);
+
+                this.addShapes([bg, pie, shadow, check, frame]);
+                this.pie = pie;
+                this.check = check;
+            },
+
+            setValue: function(value) {
+                this.pie.setAngle(-360 * (value - 1) / 8);
+                this.check.setVisible(value == 9);
+            }
+        });
+
+        /**
+         * @command Progress
+         * @description 设置节点的进度信息(添加一个进度小图标)
+         * @param {number} value 要设置的进度
+         *     取值为 0 移除进度信息;
+         *     取值为 1 表示未开始;
+         *     取值为 2 表示完成 1/8;
+         *     取值为 3 表示完成 2/8;
+         *     取值为 4 表示完成 3/8;
+         *     其余类推,取值为 9 表示全部完成
+         * @state
+         *    0: 当前有选中的节点
+         *   -1: 当前没有选中的节点
+         */
+        var ProgressCommand = kity.createClass('ProgressCommand', {
+            base: Command,
+            execute: function(km, value) {
+                var nodes = km.getSelectedNodes();
+                for (var i = 0; i < nodes.length; i++) {
+                    nodes[i].setData(PROGRESS_DATA, value || null).render();
+                }
+                km.layout();
+            },
+            queryValue: function(km) {
+                var nodes = km.getSelectedNodes();
+                var val;
+                for (var i = 0; i < nodes.length; i++) {
+                    val = nodes[i].getData(PROGRESS_DATA);
+                    if (val) break;
+                }
+                return val || null;
+            },
+
+            queryState: function(km) {
+                return km.getSelectedNodes().length ? 0 : -1;
+            }
+        });
+
+        return {
+            'commands': {
+                'progress': ProgressCommand
+            },
+            'renderers': {
+                left: kity.createClass('ProgressRenderer', {
+                    base: Renderer,
+
+                    create: function(node) {
+                        return new ProgressIcon();
+                    },
+
+                    shouldRender: function(node) {
+                        return node.getData(PROGRESS_DATA);
+                    },
+
+                    update: function(icon, node, box) {
+                        var data = node.getData(PROGRESS_DATA);
+                        var spaceLeft = node.getStyle('space-left');
+                        var x, y;
+
+                        icon.setValue(data);
+
+                        x = box.left - icon.width - spaceLeft;
+                        y = -icon.height / 2;
+                        icon.setTranslate(x + icon.width / 2, y + icon.height / 2);
+
+                        return new kity.Box(x, y, icon.width, icon.height);
+                    }
+                })
+            }
+        };
+    });
+});

+ 314 - 0
kityminder-editor/src/module/resource.js

@@ -0,0 +1,314 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('Resource', function() {
+
+        // String Hash
+        // https://github.com/drostie/sha3-js/edit/master/blake32.min.js
+        var blake32=(function(){var k,g,r,l,m,o,p,q,t,w,x;x=4*(1<<30);k=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];m=[0x243F6A88,0x85A308D3,0x13198A2E,0x03707344,0xA4093822,0x299F31D0,0x082EFA98,0xEC4E6C89,0x452821E6,0x38D01377,0xBE5466CF,0x34E90C6C,0xC0AC29B7,0xC97C50DD,0x3F84D5B5,0xB5470917];w=function(i){if(i<0){i+=x}return("00000000"+i.toString(16)).slice(-8)};o=[[16,50,84,118,152,186,220,254],[174,132,249,109,193,32,123,53],[139,12,37,223,234,99,23,73],[151,19,205,235,98,165,4,143],[9,117,66,250,30,203,134,211],[194,166,176,56,212,87,239,145],[92,241,222,164,112,54,41,184],[189,231,28,147,5,79,104,162],[246,158,59,128,44,125,65,90],[42,72,103,81,191,233,195,13]];p=function(a,b,n){var s=q[a]^q[b];q[a]=(s>>>n)|(s<<(32-n))};g=function(i,a,b,c,d){var u=l+o[r][i]%16,v=l+(o[r][i]>>4);a%=4;b=4+b%4;c=8+c%4;d=12+d%4;q[a]+=q[b]+(t[u]^m[v%16]);p(d,a,16);q[c]+=q[d];p(b,c,12);q[a]+=q[b]+(t[v]^m[u%16]);p(d,a,8);q[c]+=q[d];p(b,c,7)};return function(a,b){if(!(b instanceof Array&&b.length===4)){b=[0,0,0,0]}var c,d,e,L,f,h,j,i;d=k.slice(0);c=m.slice(0,8);for(r=0;r<4;r+=1){c[r]^=b[r]}e=a.length*16;f=(e%512>446||e%512===0)?0:e;if(e%512===432){a+="\u8001"}else{a+="\u8000";while(a.length%32!==27){a+="\u0000"}a+="\u0001"}t=[];for(i=0;i<a.length;i+=2){t.push(a.charCodeAt(i)*65536+a.charCodeAt(i+1))}t.push(0);t.push(e);h=t.length-16;j=0;for(l=0;l<t.length;l+=16){j+=512;L=(l===h)?f:Math.min(e,j);q=d.concat(c);q[12]^=L;q[13]^=L;for(r=0;r<10;r+=1){for(i=0;i<8;i+=1){if(i<4){g(i,i,i,i,i)}else{g(i,i,i+1,i+2,i+3)}}}for(i=0;i<8;i+=1){d[i]^=b[i%4]^q[i]^q[i+8]}}return d.map(w).join("")}}());
+
+        /**
+         * 自动使用的颜色序列
+         */
+        var RESOURCE_COLOR_SERIES = [51, 303, 75, 200, 157, 0, 26, 254].map(function(h) {
+            return kity.Color.createHSL(h, 100, 85);
+        });
+
+        /**
+         * 在 Minder 上拓展一些关于资源的支持接口
+         */
+        kity.extendClass(Minder, {
+
+            /**
+             * 获取字符串的哈希值
+             *
+             * @param {String} str
+             * @return {Number} hashCode
+             */
+            getHashCode: function(str) {
+                str = blake32(str);
+                var hash = 1315423911, i, ch;
+                for (i = str.length - 1; i >= 0; i--) {
+                    ch = str.charCodeAt(i);
+                    hash ^= ((hash << 5) + ch + (hash >> 2));
+                }
+                return  (hash & 0x7FFFFFFF);
+            },
+
+            /**
+             * 获取脑图中某个资源对应的颜色
+             *
+             * 如果存在同名资源,则返回已经分配给该资源的颜色,否则分配给该资源一个颜色,并且返回
+             *
+             * 如果资源数超过颜色序列数量,返回哈希颜色
+             *
+             * @param {String} resource 资源名称
+             * @return {Color}
+             */
+            getResourceColor: function(resource) {
+                var colorMapping = this._getResourceColorIndexMapping();
+                var nextIndex;
+
+                if (!Object.prototype.hasOwnProperty.call(colorMapping, resource)) {
+                    // 找不到找下个可用索引
+                    nextIndex = this._getNextResourceColorIndex();
+                    colorMapping[resource] = nextIndex;
+                }
+
+                // 资源过多,找不到可用索引颜色,统一返回哈希函数得到的颜色
+                return RESOURCE_COLOR_SERIES[colorMapping[resource]] || kity.Color.createHSL(Math.floor(this.getHashCode(resource) / 0x7FFFFFFF * 359), 100, 85);
+            },
+
+            /**
+             * 获得已使用的资源的列表
+             *
+             * @return {Array}
+             */
+            getUsedResource: function() {
+                var mapping = this._getResourceColorIndexMapping();
+                var used = [],
+                    resource;
+
+                for (resource in mapping) {
+                    if (Object.prototype.hasOwnProperty.call(mapping, resource)) {
+                        used.push(resource);
+                    }
+                }
+
+                return used;
+            },
+
+            /**
+             * 获取脑图下一个可用的资源颜色索引
+             *
+             * @return {int}
+             */
+            _getNextResourceColorIndex: function() {
+                // 获取现有颜色映射
+                //     resource => color_index
+                var colorMapping = this._getResourceColorIndexMapping();
+
+                var resource, used, i;
+
+                used = [];
+
+                // 抽取已经使用的值到 used 数组
+                for (resource in colorMapping) {
+                    if (Object.prototype.hasOwnProperty.call(colorMapping, resource)) {
+                        used.push(colorMapping[resource]);
+                    }
+                }
+
+                // 枚举所有的可用值,如果还没被使用,返回
+                for (i = 0; i < RESOURCE_COLOR_SERIES.length; i++) {
+                    if (!~used.indexOf(i)) return i;
+                }
+
+                // 没有可用的颜色了
+                return -1;
+            },
+
+            // 获取现有颜色映射
+            //     resource => color_index
+            _getResourceColorIndexMapping: function() {
+                return this._resourceColorMapping || (this._resourceColorMapping = {});
+            }
+
+        });
+
+
+        /**
+         * @class 设置资源的命令
+         *
+         * @example
+         *
+         * // 设置选中节点资源为 "张三"
+         * minder.execCommand('resource', ['张三']);
+         *
+         * // 添加资源 "李四" 到选中节点
+         * var resource = minder.queryCommandValue();
+         * resource.push('李四');
+         * minder.execCommand('resource', resource);
+         *
+         * // 清除选中节点的资源
+         * minder.execCommand('resource', null);
+         */
+        var ResourceCommand = kity.createClass('ResourceCommand', {
+
+            base: Command,
+
+            execute: function(minder, resource) {
+                var nodes = minder.getSelectedNodes();
+
+                if (typeof(resource) == 'string') {
+                    resource = [resource];
+                }
+
+                nodes.forEach(function(node) {
+                    node.setData('resource', resource).render();
+                });
+
+                minder.layout(200);
+            },
+
+            queryValue: function(minder) {
+                var nodes = minder.getSelectedNodes();
+                var resource = [];
+
+                nodes.forEach(function(node) {
+                    var nodeResource = node.getData('resource');
+
+                    if (!nodeResource) return;
+
+                    nodeResource.forEach(function(name) {
+                        if (!~resource.indexOf(name)) {
+                            resource.push(name);
+                        }
+                    });
+                });
+
+                return resource;
+            },
+
+            queryState: function(km) {
+                return km.getSelectedNode() ? 0 : -1;
+            }
+        });
+
+        /**
+         * @class 资源的覆盖图形
+         *
+         * 该类为一个资源以指定的颜色渲染一个动态的覆盖图形
+         */
+        var ResourceOverlay = kity.createClass('ResourceOverlay', {
+            base: kity.Group,
+
+            constructor: function() {
+                this.callBase();
+
+                var text, rect;
+
+                rect = this.rect = new kity.Rect().setRadius(4);
+
+                text = this.text = new kity.Text()
+                    .setFontSize(12)
+                    .setVerticalAlign('middle');
+
+                this.addShapes([rect, text]);
+            },
+
+            setValue: function(resourceName, color) {
+                var paddingX = 8,
+                    paddingY = 4,
+                    borderRadius = 4;
+                var text, box, rect;
+
+                text = this.text;
+
+                if (resourceName == this.lastResourceName) {
+
+                    box = this.lastBox;
+
+                } else {
+
+                    text.setContent(resourceName);
+
+                    box = text.getBoundaryBox();
+                    this.lastResourceName = resourceName;
+                    this.lastBox = box;
+
+                }
+
+                text.setX(paddingX).fill(color.dec('l', 70));
+
+                rect = this.rect;
+                rect.setPosition(0, box.y - paddingY);
+                this.width = Math.round(box.width + paddingX * 2);
+                this.height = Math.round(box.height + paddingY * 2);
+                rect.setSize(this.width, this.height);
+                rect.fill(color);
+            }
+        });
+
+        /**
+         * @class 资源渲染器
+         */
+        var ResourceRenderer = kity.createClass('ResourceRenderer', {
+            base: Renderer,
+
+            create: function(node) {
+                this.overlays = [];
+                return new kity.Group();
+            },
+
+            shouldRender: function(node) {
+                return node.getData('resource') && node.getData('resource').length;
+            },
+
+            update: function(container, node, box) {
+                var spaceRight = node.getStyle('space-right');
+
+                var overlays = this.overlays;
+
+                /*  修复 resource 数组中出现 null 的 bug
+                 *  @Author zhangbobell
+                 *  @date 2016-01-15
+                 */
+                var resource = node.getData("resource").filter(function(ele) {
+                    return ele !== null;
+                });
+                if (resource.length === 0) {
+                    return;
+                }
+                var minder = node.getMinder();
+                var i, overlay, x;
+
+                x = 0;
+                for (i = 0; i < resource.length; i++) {
+
+                    x += spaceRight;
+
+                    overlay = overlays[i];
+                    if (!overlay) {
+                        overlay = new ResourceOverlay();
+                        overlays.push(overlay);
+                        container.addShape(overlay);
+                    }
+                    overlay.setVisible(true);
+                    overlay.setValue(resource[i], minder.getResourceColor(resource[i]));
+                    overlay.setTranslate(x, -1);
+
+                    x += overlay.width;
+                }
+
+                while ((overlay = overlays[i++])) overlay.setVisible(false);
+
+                container.setTranslate(box.right, 0);
+
+                return new kity.Box({
+                    x: box.right,
+                    y: Math.round(-overlays[0].height / 2),
+                    width: x,
+                    height: overlays[0].height
+                });
+            }
+        });
+
+        return {
+            commands: {
+                'resource': ResourceCommand
+            },
+
+            renderers: {
+                right: ResourceRenderer
+            }
+        };
+    });
+});

+ 184 - 0
kityminder-editor/src/module/select.js

@@ -0,0 +1,184 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('Select', function() {
+        var minder = this;
+        var rc = minder.getRenderContainer();
+
+        // 在实例上渲染框选矩形、计算框选范围的对象
+        var marqueeActivator = (function() {
+
+            // 记录选区的开始位置(mousedown的位置)
+            var startPosition = null;
+
+            // 选区的图形
+            var marqueeShape = new kity.Path();
+
+            // 标记是否已经启动框选状态
+            //    并不是 mousedown 发生之后就启动框选状态,而是检测到移动了一定的距离(MARQUEE_MODE_THRESHOLD)之后
+            var marqueeMode = false;
+            var MARQUEE_MODE_THRESHOLD = 10;
+
+            return {
+                selectStart: function(e) {
+                    // 只接受左键
+                    if (e.originEvent.button || e.originEvent.altKey) return;
+
+                    // 清理不正确状态
+                    if (startPosition) {
+                        return this.selectEnd();
+                    }
+
+                    startPosition = e.getPosition(rc).round();
+                },
+                selectMove: function(e) {
+                    if (minder.getStatus() == 'textedit') {
+                        return;
+                    }
+                    if (!startPosition) return;
+
+                    var p1 = startPosition,
+                        p2 = e.getPosition(rc);
+
+                    // 检测是否要进入选区模式
+                    if (!marqueeMode) {
+                        // 距离没达到阈值,退出
+                        if (kity.Vector.fromPoints(p1, p2).length() < MARQUEE_MODE_THRESHOLD) {
+                            return;
+                        }
+                        // 已经达到阈值,记录下来并且重置选区形状
+                        marqueeMode = true;
+                        rc.addShape(marqueeShape);
+                        marqueeShape
+                            .fill(minder.getStyle('marquee-background'))
+                            .stroke(minder.getStyle('marquee-stroke')).setOpacity(0.8).getDrawer().clear();
+                    }
+
+                    var marquee = new kity.Box(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y),
+                        selectedNodes = [];
+
+                    // 使其犀利
+                    marquee.left = Math.round(marquee.left);
+                    marquee.top = Math.round(marquee.top);
+                    marquee.right = Math.round(marquee.right);
+                    marquee.bottom = Math.round(marquee.bottom);
+
+                    // 选区形状更新
+                    marqueeShape.getDrawer().pipe(function() {
+                        this.clear();
+                        this.moveTo(marquee.left, marquee.top);
+                        this.lineTo(marquee.right, marquee.top);
+                        this.lineTo(marquee.right, marquee.bottom);
+                        this.lineTo(marquee.left, marquee.bottom);
+                        this.close();
+                    });
+
+                    // 计算选中范围
+                    minder.getRoot().traverse(function(node) {
+                        var renderBox = node.getLayoutBox();
+                        if (!renderBox.intersect(marquee).isEmpty()) {
+                            selectedNodes.push(node);
+                        }
+                    });
+
+                    // 应用选中范围
+                    minder.select(selectedNodes, true);
+
+                    // 清除多余的东西
+                    window.getSelection().removeAllRanges();
+                },
+                selectEnd: function(e) {
+                    if (startPosition) {
+                        startPosition = null;
+                    }
+                    if (marqueeMode) {
+                        marqueeShape.fadeOut(200, 'ease', 0, function() {
+                            if (marqueeShape.remove) marqueeShape.remove();
+                        });
+                        marqueeMode = false;
+                    }
+                }
+            };
+        })();
+
+        var lastDownNode = null,
+            lastDownPosition = null;
+        return {
+            'init': function() {
+                window.addEventListener('mouseup', function() {
+                    marqueeActivator.selectEnd();
+                });
+            },
+            'events': {
+                'mousedown': function(e) {
+
+                    var downNode = e.getTargetNode();
+
+                    // 没有点中节点:
+                    //     清除选中状态,并且标记选区开始位置
+                    if (!downNode) {
+                        this.removeAllSelectedNodes();
+                        marqueeActivator.selectStart(e);
+
+                        this.setStatus('normal');
+                    }
+
+                    // 点中了节点,并且按了 shift 键:
+                    //     被点中的节点切换选中状态
+                    else if (e.isShortcutKey('Ctrl')) {
+                        this.toggleSelect(downNode);
+                    }
+
+                    // 点中的节点没有被选择:
+                    //     单选点中的节点
+                    else if (!downNode.isSelected()) {
+                        this.select(downNode, true);
+                    }
+
+                    // 点中的节点被选中了,并且不是单选:
+                    //     完成整个点击之后需要使其变为单选。
+                    //     不能马上变为单选,因为可能是需要拖动选中的多个节点
+                    else if (!this.isSingleSelect()) {
+                        lastDownNode = downNode;
+                        lastDownPosition = e.getPosition();
+                    }
+                },
+                'mousemove': marqueeActivator.selectMove,
+                'mouseup': function(e) {
+                    var upNode = e.getTargetNode();
+
+                    // 如果 mouseup 发生在 lastDownNode 外,是无需理会的
+                    if (upNode && upNode == lastDownNode) {
+                        var upPosition = e.getPosition();
+                        var movement = kity.Vector.fromPoints(lastDownPosition, upPosition);
+                        if (movement.length() < 1) this.select(lastDownNode, true);
+                        lastDownNode = null;
+                    }
+
+                    // 清理一下选择状态
+                    marqueeActivator.selectEnd(e);
+                },
+                //全选操作
+                'normal.keydown': function(e) {
+
+                    if (e.isShortcutKey('ctrl+a')) {
+                        var selectedNodes = [];
+
+                        this.getRoot().traverse(function(node) {
+                            selectedNodes.push(node);
+                        });
+                        this.select(selectedNodes, true);
+                        e.preventDefault();
+                    }
+                }
+            }
+        };
+    });
+});

+ 114 - 0
kityminder-editor/src/module/style.js

@@ -0,0 +1,114 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('StyleModule', function() {
+        var styleNames = ['font-size', 'font-family', 'font-weight', 'font-style', 'background', 'color'];
+        var styleClipBoard = null;
+
+        function hasStyle(node) {
+            var data = node.getData();
+            for (var i = 0; i < styleNames.length; i++) {
+                if (styleNames[i] in data) return true;
+            }
+        }
+
+        return {
+            'commands': {
+                /**
+                 * @command CopyStyle
+                 * @description 拷贝选中节点的当前样式,包括字体、字号、粗体、斜体、背景色、字体色
+                 * @state
+                 *   0: 当前有选中的节点
+                 *  -1: 当前没有选中的节点
+                 */
+                'copystyle': kity.createClass('CopyStyleCommand', {
+                    base: Command,
+
+                    execute: function(minder) {
+                        var node = minder.getSelectedNode();
+                        var nodeData = node.getData();
+                        styleClipBoard = {};
+                        styleNames.forEach(function(name) {
+                            if (name in nodeData) styleClipBoard[name] = nodeData[name];
+                            else {
+                                styleClipBoard[name] = null;
+                                delete styleClipBoard[name];
+                            }
+                        });
+                        return styleClipBoard;
+                    },
+
+                    queryState: function(minder) {
+                        var nodes = minder.getSelectedNodes();
+                        if (nodes.length !== 1) return -1;
+                        return hasStyle(nodes[0]) ? 0 : -1;
+                    }
+                }),
+
+                /**
+                 * @command PasteStyle
+                 * @description 粘贴已拷贝的样式到选中的节点上,包括字体、字号、粗体、斜体、背景色、字体色
+                 * @state
+                 *   0: 当前有选中的节点,并且已经有复制的样式
+                 *  -1: 当前没有选中的节点,或者没有复制的样式
+                 */
+                'pastestyle': kity.createClass('PastStyleCommand', {
+                    base: Command,
+
+                    execute: function(minder) {
+                        minder.getSelectedNodes().forEach(function(node) {
+                            for (var name in styleClipBoard) {
+                                if (styleClipBoard.hasOwnProperty(name))
+                                    node.setData(name, styleClipBoard[name]);
+                            }
+                        });
+                        minder.renderNodeBatch(minder.getSelectedNodes());
+                        minder.layout(300);
+                        return styleClipBoard;
+                    },
+
+                    queryState: function(minder) {
+                        return (styleClipBoard && minder.getSelectedNodes().length) ? 0 : -1;
+                    }
+                }),
+
+                /**
+                 * @command ClearStyle
+                 * @description 移除选中节点的样式,包括字体、字号、粗体、斜体、背景色、字体色
+                 * @state
+                 *   0: 当前有选中的节点,并且至少有一个设置了至少一种样式
+                 *  -1: 其它情况
+                 */
+                'clearstyle': kity.createClass('ClearStyleCommand', {
+                    base: Command,
+                    execute: function(minder) {
+                        minder.getSelectedNodes().forEach(function(node) {
+                            styleNames.forEach(function(name) {
+                                node.setData(name);
+                            });
+                        });
+                        minder.renderNodeBatch(minder.getSelectedNodes());
+                        minder.layout(300);
+                        return styleClipBoard;
+                    },
+
+                    queryState: function(minder) {
+                        var nodes = minder.getSelectedNodes();
+                        if (!nodes.length) return -1;
+                        for (var i = 0; i < nodes.length; i++) {
+                            if (hasStyle(nodes[i])) return 0;
+                        }
+                        return -1;
+                    }
+                })
+            }
+        };
+    });
+});

+ 288 - 0
kityminder-editor/src/module/text.js

@@ -0,0 +1,288 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+    /**
+     * 针对不同系统、不同浏览器、不同字体做居中兼容性处理
+     * 暂时未增加Linux的处理
+     */
+    var FONT_ADJUST = {
+        'safari': {
+            '微软雅黑,Microsoft YaHei': -0.17,
+            '楷体,楷体_GB2312,SimKai': -0.1,
+            '隶书, SimLi': -0.1,
+            'comic sans ms': -0.23,
+            'impact,chicago': -0.15,
+            'times new roman': -0.1,
+            'arial black,avant garde': -0.17,
+            'default': 0
+        },
+        'ie': {
+            10: {
+                '微软雅黑,Microsoft YaHei': -0.17,
+                'comic sans ms': -0.17,
+                'impact,chicago': -0.08,
+                'times new roman': 0.04,
+                'arial black,avant garde': -0.17,
+                'default': -0.15
+            },
+            11: {
+                '微软雅黑,Microsoft YaHei': -0.17,
+                'arial,helvetica,sans-serif': -0.17,
+                'comic sans ms': -0.17,
+                'impact,chicago': -0.08,
+                'times new roman': 0.04,
+                'sans-serif': -0.16,
+                'arial black,avant garde': -0.17,
+                'default': -0.15
+            }
+        },
+        'edge': {
+            '微软雅黑,Microsoft YaHei': -0.15,
+            'arial,helvetica,sans-serif': -0.17,
+            'comic sans ms': -0.17,
+            'impact,chicago': -0.08,
+            'sans-serif': -0.16,
+            'arial black,avant garde': -0.17,
+            'default': -0.15
+        },
+        'sg': {
+            '微软雅黑,Microsoft YaHei': -0.15,
+            'arial,helvetica,sans-serif': -0.05,
+            'comic sans ms': -0.22,
+            'impact,chicago': -0.16,
+            'times new roman': -0.03,
+            'arial black,avant garde': -0.22,
+            'default': -0.15
+        },
+        'chrome': {
+            'Mac': {
+                'andale mono': -0.05,
+                'comic sans ms': -0.3,
+                'impact,chicago': -0.13,
+                'times new roman': -0.1,
+                'arial black,avant garde': -0.17,
+                'default': 0
+            },
+            'Win': {
+                '微软雅黑,Microsoft YaHei': -0.15,
+                'arial,helvetica,sans-serif': -0.02,
+                'arial black,avant garde': -0.2,
+                'comic sans ms': -0.2,
+                'impact,chicago': -0.12,
+                'times new roman': -0.02,
+                'default': -0.15
+            },
+            'Lux': {
+                'andale mono': -0.05,
+                'comic sans ms': -0.3,
+                'impact,chicago': -0.13,
+                'times new roman': -0.1,
+                'arial black,avant garde': -0.17,
+                'default': 0
+            }
+        },
+        'firefox': {
+            'Mac': {
+                '微软雅黑,Microsoft YaHei': -0.2,
+                '宋体,SimSun': 0.05,
+                'comic sans ms': -0.2,
+                'impact,chicago': -0.15,
+                'arial black,avant garde': -0.17,
+                'times new roman': -0.1,
+                'default': 0.05
+            },
+            'Win': {
+                '微软雅黑,Microsoft YaHei': -0.16,
+                'andale mono': -0.17,
+                'arial,helvetica,sans-serif': -0.17,
+                'comic sans ms': -0.22,
+                'impact,chicago': -0.23,
+                'times new roman': -0.22,
+                'sans-serif': -0.22,
+                'arial black,avant garde': -0.17,
+                'default': -0.16
+            },
+            'Lux': {
+                "宋体,SimSun": -0.2,
+                "微软雅黑,Microsoft YaHei": -0.2,
+                "黑体, SimHei": -0.2,
+                "隶书, SimLi": -0.2,
+                "楷体,楷体_GB2312,SimKai": -0.2,
+                "andale mono": -0.2,
+                "arial,helvetica,sans-serif": -0.2,
+                "comic sans ms": -0.2,
+                "impact,chicago": -0.2,
+                "times new roman": -0.2,
+                "sans-serif": -0.2,
+                "arial black,avant garde": -0.2,
+                "default": -0.16
+            }
+        },
+    };
+
+    var TextRenderer = kity.createClass('TextRenderer', {
+        base: Renderer,
+
+        create: function() {
+            return new kity.Group().setId(utils.uuid('node_text'));
+        },
+
+        update: function(textGroup, node) {
+
+            function getDataOrStyle(name) {
+                return node.getData(name) || node.getStyle(name);
+            }
+
+            var nodeText = node.getText();
+            var textArr = nodeText ? nodeText.split('\n') : [' '];
+
+            var lineHeight = node.getStyle('line-height');
+
+            var fontSize = getDataOrStyle('font-size');
+            var fontFamily = getDataOrStyle('font-family') || 'default';
+
+            var height = (lineHeight * fontSize) * textArr.length - (lineHeight - 1) * fontSize;
+            var yStart = -height / 2;
+            var Browser = kity.Browser;
+            var adjust;
+
+            if (Browser.chrome || Browser.opera || Browser.bd ||Browser.lb === "chrome") {
+                adjust = FONT_ADJUST['chrome'][Browser.platform][fontFamily];
+            } else if (Browser.gecko) {
+                adjust = FONT_ADJUST['firefox'][Browser.platform][fontFamily];
+            } else if (Browser.sg) {
+                adjust = FONT_ADJUST['sg'][fontFamily];
+            } else if (Browser.safari) {
+                adjust = FONT_ADJUST['safari'][fontFamily];
+            } else if (Browser.ie) {
+                adjust = FONT_ADJUST['ie'][Browser.version][fontFamily];
+            } else if (Browser.edge) {
+                adjust = FONT_ADJUST['edge'][fontFamily];
+            } else if (Browser.lb) {
+                // 猎豹浏览器的ie内核兼容性模式下
+                adjust = 0.9;
+            }
+
+            textGroup.setTranslate(0, (adjust || 0) * fontSize);
+
+            var rBox = new kity.Box(),
+                r = Math.round;
+
+            this.setTextStyle(node, textGroup);
+
+            var textLength = textArr.length;
+
+            var textGroupLength = textGroup.getItems().length;
+
+            var i, ci, textShape, text;
+
+            if (textLength < textGroupLength) {
+                for (i = textLength, ci; ci = textGroup.getItem(i);) {
+                    textGroup.removeItem(i);
+                }
+            } else if (textLength > textGroupLength) {
+                var growth = textLength - textGroupLength;
+                while (growth--) {
+                    textShape = new kity.Text()
+                        .setAttr('text-rendering', 'inherit');
+                    if (kity.Browser.ie || kity.Browser.edge) {
+                        textShape.setVerticalAlign('top');
+                    } else {
+                        textShape.setAttr('dominant-baseline', 'text-before-edge');
+                    }
+                    textGroup.addItem(textShape);
+                }
+            }
+
+            for (i = 0, text, textShape;
+                (text = textArr[i], textShape = textGroup.getItem(i)); i++) {
+                textShape.setContent(text);
+                if (kity.Browser.ie || kity.Browser.edge) {
+                    textShape.fixPosition();
+                }
+            }
+
+            this.setTextStyle(node, textGroup);
+
+            var textHash = node.getText() +
+                ['font-size', 'font-name', 'font-weight', 'font-style'].map(getDataOrStyle).join('/');
+
+            if (node._currentTextHash == textHash && node._currentTextGroupBox) return node._currentTextGroupBox;
+
+            node._currentTextHash = textHash;
+
+            return function() {
+                textGroup.eachItem(function(i, textShape) {
+                    var y = yStart + i * fontSize * lineHeight;
+
+                    textShape.setY(y);
+                    var bbox = textShape.getBoundaryBox();
+                    rBox = rBox.merge(new kity.Box(0, y, bbox.height && bbox.width || 1, fontSize));
+                });
+
+                var nBox = new kity.Box(r(rBox.x), r(rBox.y), r(rBox.width), r(rBox.height));
+
+                node._currentTextGroupBox = nBox;
+                return nBox;
+            };
+
+        },
+
+        setTextStyle: function(node, text) {
+            var hooks = TextRenderer._styleHooks;
+            hooks.forEach(function(hook) {
+                hook(node, text);
+            });
+        }
+    });
+
+    var TextCommand = kity.createClass({
+        base: Command,
+        execute: function(minder, text) {
+            var node = minder.getSelectedNode();
+            if (node) {
+                node.setText(text);
+                node.render();
+                minder.layout();
+            }
+        },
+        queryState: function(minder) {
+            return minder.getSelectedNodes().length == 1 ? 0 : -1;
+        },
+        queryValue: function(minder) {
+            var node = minder.getSelectedNode();
+            return node ? node.getText() : null;
+        }
+    });
+
+    utils.extend(TextRenderer, {
+        _styleHooks: [],
+
+        registerStyleHook: function(fn) {
+            TextRenderer._styleHooks.push(fn);
+        }
+    });
+
+    kity.extendClass(MinderNode, {
+        getTextGroup: function() {
+            return this.getRenderer('TextRenderer').getRenderShape();
+        }
+    });
+
+    Module.register('text', {
+        'commands': {
+            'text': TextCommand
+        },
+        'renderers': {
+            center: TextRenderer
+        }
+    });
+
+    module.exports = TextRenderer;
+});

+ 397 - 0
kityminder-editor/src/module/view.js

@@ -0,0 +1,397 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    var ViewDragger = kity.createClass('ViewDragger', {
+        constructor: function(minder) {
+            this._minder = minder;
+            this._enabled = false;
+            this._bind();
+            var me = this;
+            this._minder.getViewDragger = function() {
+                return me;
+            };
+            this.setEnabled(false);
+        },
+
+        isEnabled: function() {
+            return this._enabled;
+        },
+
+        setEnabled: function(value) {
+            var paper = this._minder.getPaper();
+            paper.setStyle('cursor', value ? 'pointer' : 'default');
+            paper.setStyle('cursor', value ? '-webkit-grab' : 'default');
+            this._enabled = value;
+        },
+        timeline: function() {
+            return this._moveTimeline;
+        },
+
+        move: function(offset, duration) {
+            var minder = this._minder;
+
+            var targetPosition = this.getMovement().offset(offset);
+
+            this.moveTo(targetPosition, duration);
+        },
+
+        moveTo: function(position, duration) {
+
+            if (duration) {
+                var dragger = this;
+
+                if (this._moveTimeline) this._moveTimeline.stop();
+
+                this._moveTimeline = this._minder.getRenderContainer().animate(new kity.Animator(
+                    this.getMovement(),
+                    position,
+                    function(target, value) {
+                        dragger.moveTo(value);
+                    }
+                ), duration, 'easeOutCubic').timeline();
+
+                this._moveTimeline.on('finish', function() {
+                    dragger._moveTimeline = null;
+                });
+
+                return this;
+            }
+
+            this._minder.getRenderContainer().setTranslate(position.round());
+            this._minder.fire('viewchange');
+        },
+
+        getMovement: function() {
+            var translate = this._minder.getRenderContainer().transform.translate;
+            return translate ? translate[0] : new kity.Point();
+        },
+
+        getView: function() {
+            var minder = this._minder;
+            var c = minder._lastClientSize || {
+                width: minder.getRenderTarget().clientWidth,
+                height: minder.getRenderTarget().clientHeight
+            };
+            var m = this.getMovement();
+            var box = new kity.Box(0, 0, c.width, c.height);
+            var viewMatrix = minder.getPaper().getViewPortMatrix();
+            return viewMatrix.inverse().translate(-m.x, -m.y).transformBox(box);
+        },
+
+        _bind: function() {
+            var dragger = this,
+                isTempDrag = false,
+                lastPosition = null,
+                currentPosition = null;
+
+            function dragEnd(e) {
+                if (!lastPosition) return;
+
+
+                lastPosition = null;
+
+                e.stopPropagation();
+
+                // 临时拖动需要还原状态
+                if (isTempDrag) {
+                    dragger.setEnabled(false);
+                    isTempDrag = false;
+                    if (dragger._minder.getStatus() == 'hand')
+                        dragger._minder.rollbackStatus();
+                }
+                var paper = dragger._minder.getPaper();
+                paper.setStyle('cursor', dragger._minder.getStatus() == 'hand' ? '-webkit-grab' : 'default');
+
+                dragger._minder.fire('viewchanged');
+            }
+
+            this._minder.on('normal.mousedown normal.touchstart ' +
+                'inputready.mousedown inputready.touchstart ' +
+                'readonly.mousedown readonly.touchstart',
+                function(e) {
+                    if (e.originEvent.button == 2) {
+                        e.originEvent.preventDefault(); // 阻止中键拉动
+                    }
+                    // 点击未选中的根节点临时开启
+                    if (e.getTargetNode() == this.getRoot() || e.originEvent.button == 2 || e.originEvent.altKey) {
+                        lastPosition = e.getPosition('view');
+                        isTempDrag = true;
+                    }
+                })
+
+            .on('normal.mousemove normal.touchmove ' +
+                'readonly.mousemove readonly.touchmove ' +
+                'inputready.mousemove inputready.touchmove',
+                function(e) {
+                    if (e.type == 'touchmove') {
+                        e.preventDefault(); // 阻止浏览器的后退事件
+                    }
+                    if (!isTempDrag) return;
+                    var offset = kity.Vector.fromPoints(lastPosition, e.getPosition('view'));
+                    if (offset.length() > 10) {
+                        this.setStatus('hand', true);
+                        var paper = dragger._minder.getPaper();
+                        paper.setStyle('cursor', '-webkit-grabbing');
+                    }
+                })
+
+            .on('hand.beforemousedown hand.beforetouchstart', function(e) {
+                // 已经被用户打开拖放模式
+                if (dragger.isEnabled()) {
+                    lastPosition = e.getPosition('view');
+                    e.stopPropagation();
+                    var paper = dragger._minder.getPaper();
+                    paper.setStyle('cursor', '-webkit-grabbing');
+                }
+            })
+
+            .on('hand.beforemousemove hand.beforetouchmove', function(e) {
+                if (lastPosition) {
+                    currentPosition = e.getPosition('view');
+
+                    // 当前偏移加上历史偏移
+                    var offset = kity.Vector.fromPoints(lastPosition, currentPosition);
+                    dragger.move(offset);
+                    e.stopPropagation();
+                    e.preventDefault();
+                    e.originEvent.preventDefault();
+                    lastPosition = currentPosition;
+                }
+            })
+
+            .on('mouseup touchend', dragEnd);
+
+            window.addEventListener('mouseup', dragEnd);
+            this._minder.on('contextmenu', function(e) {
+                e.preventDefault();
+            });
+        }
+    });
+
+    Module.register('View', function() {
+
+        var km = this;
+
+        /**
+         * @command Hand
+         * @description 切换抓手状态,抓手状态下,鼠标拖动将拖动视野,而不是创建选区
+         * @state
+         *   0: 当前不是抓手状态
+         *   1: 当前是抓手状态
+         */
+        var ToggleHandCommand = kity.createClass('ToggleHandCommand', {
+            base: Command,
+            execute: function(minder) {
+
+                if (minder.getStatus() != 'hand') {
+                    minder.setStatus('hand', true);
+                } else {
+                    minder.rollbackStatus();
+                }
+                this.setContentChanged(false);
+
+            },
+            queryState: function(minder) {
+                return minder.getStatus() == 'hand' ? 1 : 0;
+            },
+            enableReadOnly: true
+        });
+
+        /**
+         * @command Camera
+         * @description 设置当前视野的中心位置到某个节点上
+         * @param {kityminder.MinderNode} focusNode 要定位的节点
+         * @param {number} duration 设置视野移动的动画时长(单位 ms),设置为 0 不使用动画
+         * @state
+         *   0: 始终可用
+         */
+        var CameraCommand = kity.createClass('CameraCommand', {
+            base: Command,
+            execute: function(km, focusNode) {
+
+                focusNode = focusNode || km.getRoot();
+                var viewport = km.getPaper().getViewPort();
+                var offset = focusNode.getRenderContainer().getRenderBox('view');
+                var dx = viewport.center.x - offset.x - offset.width / 2,
+                    dy = viewport.center.y - offset.y;
+                var dragger = km._viewDragger;
+
+                var duration = km.getOption('viewAnimationDuration');
+                dragger.move(new kity.Point(dx, dy), duration);
+                this.setContentChanged(false);
+            },
+            enableReadOnly: true
+        });
+
+        /**
+         * @command Move
+         * @description 指定方向移动当前视野
+         * @param {string} dir 移动方向
+         *    取值为 'left',视野向左移动一半
+         *    取值为 'right',视野向右移动一半
+         *    取值为 'up',视野向上移动一半
+         *    取值为 'down',视野向下移动一半
+         * @param {number} duration 视野移动的动画时长(单位 ms),设置为 0 不使用动画
+         * @state
+         *   0: 始终可用
+         */
+        var MoveCommand = kity.createClass('MoveCommand', {
+            base: Command,
+
+            execute: function(km, dir) {
+                var dragger = km._viewDragger;
+                var size = km._lastClientSize;
+                var duration = km.getOption('viewAnimationDuration');
+                switch (dir) {
+                    case 'up':
+                        dragger.move(new kity.Point(0, size.height / 2), duration);
+                        break;
+                    case 'down':
+                        dragger.move(new kity.Point(0, -size.height / 2), duration);
+                        break;
+                    case 'left':
+                        dragger.move(new kity.Point(size.width / 2, 0), duration);
+                        break;
+                    case 'right':
+                        dragger.move(new kity.Point(-size.width / 2, 0), duration);
+                        break;
+                }
+            },
+
+            enableReadOnly: true
+        });
+
+        return {
+            init: function() {
+                this._viewDragger = new ViewDragger(this);
+            },
+            commands: {
+                'hand': ToggleHandCommand,
+                'camera': CameraCommand,
+                'move': MoveCommand
+            },
+            events: {
+                statuschange: function(e) {
+                    this._viewDragger.setEnabled(e.currentStatus == 'hand');
+                },
+                mousewheel: function(e) {
+                    var dx, dy;
+                    e = e.originEvent;
+                    if (e.ctrlKey || e.shiftKey) return;
+                    if ('wheelDeltaX' in e) {
+
+                        dx = e.wheelDeltaX || 0;
+                        dy = e.wheelDeltaY || 0;
+
+                    } else {
+
+                        dx = 0;
+                        dy = e.wheelDelta;
+
+                    }
+
+                    this._viewDragger.move({
+                        x: dx / 2.5,
+                        y: dy / 2.5
+                    });
+
+                    var me = this;
+                    clearTimeout(this._mousewheeltimer);
+                    this._mousewheeltimer = setTimeout(function() {
+                        me.fire('viewchanged');
+                    }, 100);
+
+                    e.preventDefault();
+                },
+                'normal.dblclick readonly.dblclick': function(e) {
+                    if (e.kityEvent.targetShape instanceof kity.Paper) {
+                        this.execCommand('camera', this.getRoot(), 800);
+                    }
+                },
+                'paperrender finishInitHook': function() {
+                    if (!this.getRenderTarget()) {
+                        return;
+                    }
+                    this.execCommand('camera', null, 0);
+                    this._lastClientSize = {
+                        width: this.getRenderTarget().clientWidth,
+                        height: this.getRenderTarget().clientHeight
+                    };
+                },
+                resize: function(e) {
+                    var a = {
+                            width: this.getRenderTarget().clientWidth,
+                            height: this.getRenderTarget().clientHeight
+                        },
+                        b = this._lastClientSize;
+                    this._viewDragger.move(
+                        new kity.Point((a.width - b.width) / 2 | 0, (a.height - b.height) / 2 | 0));
+                    this._lastClientSize = a;
+                },
+                'selectionchange layoutallfinish': function(e) {
+                    var selected = this.getSelectedNode();
+                    var minder = this;
+
+                    /*
+                    * Added by zhangbobell 2015.9.9
+                    * windows 10 的 edge 浏览器在全部动画停止后,优先级图标不显示 text,
+                    * 因此再次触发一次 render 事件,让浏览器重绘
+                    * */
+                    if (kity.Browser.edge) {
+                        this.fire('paperrender');
+                    }
+                    if (!selected) return;
+
+                    var dragger = this._viewDragger;
+                    var timeline = dragger.timeline();
+
+                    /*
+                    * Added by zhangbobell 2015.09.25
+                    * 如果之前有动画,那么就先暂时返回,等之前动画结束之后再次执行本函数
+                    * 以防止 view 动画变动了位置,导致本函数执行的时候位置计算不对
+                    *
+                    * fixed bug : 初始化的时候中心节点位置不固定(有的时候在左上角,有的时候在中心)
+                    * */
+                    if (timeline){
+                        timeline.on('finish', function() {
+                            minder.fire('selectionchange');
+                        });
+
+                        return;
+                    }
+
+
+                    var view = dragger.getView();
+                    var focus = selected.getLayoutBox();
+                    var space = 50;
+                    var dx = 0, dy = 0;
+
+                    if (focus.right > view.right) {
+                        dx += view.right - focus.right - space;
+                    }
+                    else if (focus.left < view.left) {
+                        dx += view.left - focus.left + space;
+                    }
+
+                    if (focus.bottom > view.bottom) {
+                        dy += view.bottom - focus.bottom - space;
+                    }
+                    if (focus.top < view.top) {
+                        dy += view.top - focus.top + space;
+                    }
+
+                    if (dx || dy) dragger.move(new kity.Point(dx, dy), 100);
+
+
+                }
+            }
+        };
+    });
+});

+ 195 - 0
kityminder-editor/src/module/zoom.js

@@ -0,0 +1,195 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var utils = require('../core/utils');
+
+    var Minder = require('../core/minder');
+    var MinderNode = require('../core/node');
+    var Command = require('../core/command');
+    var Module = require('../core/module');
+    var Renderer = require('../core/render');
+
+    Module.register('Zoom', function() {
+        var me = this;
+
+        var timeline;
+
+        function setTextRendering() {
+            var value = me._zoomValue >= 100 ? 'optimize-speed' : 'geometricPrecision';
+            me.getRenderContainer().setAttr('text-rendering', value);
+        }
+
+        function fixPaperCTM(paper) {
+            var node = paper.shapeNode;
+            var ctm = node.getCTM();
+            var matrix = new kity.Matrix(ctm.a, ctm.b, ctm.c, ctm.d, (ctm.e | 0) + 0.5, (ctm.f | 0) + 0.5);
+            node.setAttribute('transform', 'matrix(' + matrix.toString() + ')');
+        }
+
+        kity.extendClass(Minder, {
+            zoom: function(value) {
+                var paper = this.getPaper();
+                var viewport = paper.getViewPort();
+                viewport.zoom = value / 100;
+                viewport.center = {
+                    x: viewport.center.x,
+                    y: viewport.center.y
+                };
+                paper.setViewPort(viewport);
+                if (value == 100) fixPaperCTM(paper);
+            },
+            getZoomValue: function() {
+                return this._zoomValue;
+            }
+        });
+
+        function zoomMinder(minder, value) {
+            var paper = minder.getPaper();
+            var viewport = paper.getViewPort();
+
+            if (!value) return;
+
+            setTextRendering();
+
+            var duration = minder.getOption('zoomAnimationDuration');
+            if (minder.getRoot().getComplex() > 200 || !duration) {
+                minder._zoomValue = value;
+                minder.zoom(value);
+                minder.fire('viewchange');
+            } else {
+                var animator = new kity.Animator({
+                    beginValue: minder._zoomValue,
+                    finishValue: value,
+                    setter: function(target, value) {
+                        target.zoom(value);
+                    }
+                });
+                minder._zoomValue = value;
+                if (timeline) {
+                    timeline.pause();
+                }
+                timeline = animator.start(minder, duration, 'easeInOutSine');
+                timeline.on('finish', function() {
+                    minder.fire('viewchange');
+                });
+            }
+            minder.fire('zoom', {
+                zoom: value
+            });
+        }
+
+        /**
+         * @command Zoom
+         * @description 缩放当前的视野到一定的比例(百分比)
+         * @param {number} value 设置的比例,取值 100 则为原尺寸
+         * @state
+         *   0: 始终可用
+         */
+        var ZoomCommand = kity.createClass('Zoom', {
+            base: Command,
+            execute: zoomMinder,
+            queryValue: function(minder) {
+                return minder._zoomValue;
+            }
+        });
+
+        /**
+         * @command ZoomIn
+         * @description 放大当前的视野到下一个比例等级(百分比)
+         * @shortcut =
+         * @state
+         *   0: 如果当前脑图的配置中还有下一个比例等级
+         *  -1: 其它情况
+         */
+        var ZoomInCommand = kity.createClass('ZoomInCommand', {
+            base: Command,
+            execute: function(minder) {
+                zoomMinder(minder, this.nextValue(minder));
+            },
+            queryState: function(minder) {
+                return +!this.nextValue(minder);
+            },
+            nextValue: function(minder) {
+                var stack = minder.getOption('zoom'),
+                    i;
+                for (i = 0; i < stack.length; i++) {
+                    if (stack[i] > minder._zoomValue) return stack[i];
+                }
+                return 0;
+            },
+            enableReadOnly: true
+        });
+
+        /**
+         * @command ZoomOut
+         * @description 缩小当前的视野到上一个比例等级(百分比)
+         * @shortcut -
+         * @state
+         *   0: 如果当前脑图的配置中还有上一个比例等级
+         *  -1: 其它情况
+         */
+        var ZoomOutCommand = kity.createClass('ZoomOutCommand', {
+            base: Command,
+            execute: function(minder) {
+                zoomMinder(minder, this.nextValue(minder));
+            },
+            queryState: function(minder) {
+                return +!this.nextValue(minder);
+            },
+            nextValue: function(minder) {
+                var stack = minder.getOption('zoom'),
+                    i;
+                for (i = stack.length - 1; i >= 0; i--) {
+                    if (stack[i] < minder._zoomValue) return stack[i];
+                }
+                return 0;
+            },
+            enableReadOnly: true
+        });
+
+        return {
+            init: function() {
+                this._zoomValue = 100;
+                this.setDefaultOptions({
+                    zoom: [10, 20, 50, 100, 200]
+                });
+                setTextRendering();
+            },
+            commands: {
+                'zoomin': ZoomInCommand,
+                'zoomout': ZoomOutCommand,
+                'zoom': ZoomCommand
+            },
+            events: {
+                'normal.mousewheel readonly.mousewheel': function(e) {
+                    if (!e.originEvent.ctrlKey && !e.originEvent.metaKey) return;
+
+                    var delta = e.originEvent.wheelDelta;
+                    var me = this;
+                    // 稀释
+                    if (Math.abs(delta) > 100) {
+                        clearTimeout(this._wheelZoomTimeout);
+                    } else {
+                        return;
+                    }
+
+                    this._wheelZoomTimeout = setTimeout(function() {
+                        var value;
+                        var lastValue = me.getPaper()._zoom || 1;
+                        if (delta > 0) {
+                            me.execCommand('zoomin');
+                        } else if (delta < 0) {
+                            me.execCommand('zoomout');
+                        }
+                    }, 100);
+                    
+                    e.originEvent.preventDefault();
+                }
+            },
+
+            commandShortcutKeys: {
+                'zoomin': 'ctrl+=',
+                'zoomout': 'ctrl+-'
+            }
+        };
+    });
+});

+ 18 - 0
kityminder-editor/src/protocol/json.js

@@ -0,0 +1,18 @@
+define(function(require, exports, module) {
+    var data = require('../core/data');
+
+    data.registerProtocol('json', module.exports = {
+        fileDescription: 'KityMinder 格式',
+        fileExtension: '.km',
+        dataType: 'text',
+        mineType: 'application/json',
+
+        encode: function(json) {
+            return JSON.stringify(json);
+        },
+
+        decode: function(local) {
+            return JSON.parse(local);
+        }
+    });
+});

+ 158 - 0
kityminder-editor/src/protocol/markdown.js

@@ -0,0 +1,158 @@
+define(function(require, exports, module) {
+    var data = require('../core/data');
+    var LINE_ENDING_SPLITER = /\r\n|\r|\n/;
+    var EMPTY_LINE = '';
+    var NOTE_MARK_START = '<!--Note-->';
+    var NOTE_MARK_CLOSE = '<!--/Note-->';
+
+    function encode(json) {
+
+        return _build(json, 1).join('\n');
+    }
+
+    function _build(node, level) {
+        var lines = [];
+
+        level = level || 1;
+
+        var sharps = _generateHeaderSharp(level);
+        lines.push(sharps + ' ' + node.data.text);
+        lines.push(EMPTY_LINE);
+
+        var note = node.data.note;
+        if (note) {
+            var hasSharp = /^#/.test(note);
+            if (hasSharp) {
+                lines.push(NOTE_MARK_START);
+                note = note.replace(/^#+/gm, function($0) {
+                    return sharps + $0;
+                });
+            }
+            lines.push(note);
+            if (hasSharp) {
+                lines.push(NOTE_MARK_CLOSE);
+            }
+            lines.push(EMPTY_LINE);
+        }
+
+        if (node.children) node.children.forEach(function(child) {
+            lines = lines.concat(_build(child, level + 1));
+        });
+
+        return lines;
+    }
+
+    function _generateHeaderSharp(level) {
+        var sharps = '';
+        while (level--) sharps += '#';
+        return sharps;
+    }
+
+    function decode(markdown) {
+
+        var json,
+            parentMap = {},
+            lines, line, lineInfo, level, node, parent, noteProgress, codeBlock;
+
+        // 一级标题转换 `{title}\n===` => `# {title}`
+        markdown = markdown.replace(/^(.+)\n={3,}/, function($0, $1) {
+            return '# ' + $1;
+        });
+
+        lines = markdown.split(LINE_ENDING_SPLITER);
+
+        // 按行分析
+        for (var i = 0; i < lines.length; i++) {
+            line = lines[i];
+
+            lineInfo = _resolveLine(line);
+
+            // 备注标记处理
+            if (lineInfo.noteClose) {
+                noteProgress = false;
+                continue;
+            } else if (lineInfo.noteStart) {
+                noteProgress = true;
+                continue;
+            }
+
+            // 代码块处理
+            codeBlock = lineInfo.codeBlock ? !codeBlock : codeBlock;
+
+            // 备注条件:备注标签中,非标题定义,或标题越位
+            if (noteProgress || codeBlock || !lineInfo.level || lineInfo.level > level + 1) {
+                if (node) _pushNote(node, line);
+                continue;
+            }
+
+            // 标题处理
+            level = lineInfo.level;
+            node = _initNode(lineInfo.content, parentMap[level - 1]);
+            parentMap[level] = node;
+        }
+
+        _cleanUp(parentMap[1]);
+        return parentMap[1];
+    }
+
+    function _initNode(text, parent) {
+        var node = {
+            data: {
+                text: text,
+                note: ''
+            }
+        };
+        if (parent) {
+            if (parent.children) parent.children.push(node);
+            else parent.children = [node];
+        }
+        return node;
+    }
+
+    function _pushNote(node, line) {
+        node.data.note += line + '\n';
+    }
+
+    function _isEmpty(line) {
+        return !/\S/.test(line);
+    }
+
+    function _resolveLine(line) {
+        var match = /^(#+)?\s*(.*)$/.exec(line);
+        return {
+            level: match[1] && match[1].length || null,
+            content: match[2],
+            noteStart: line == NOTE_MARK_START,
+            noteClose: line == NOTE_MARK_CLOSE,
+            codeBlock: /^\s*```/.test(line)
+        };
+    }
+
+    function _cleanUp(node) {
+        if (!/\S/.test(node.data.note)) {
+            node.data.note = null;
+            delete node.data.note;
+        } else {
+            var notes = node.data.note.split('\n');
+            while (notes.length && !/\S/.test(notes[0])) notes.shift();
+            while (notes.length && !/\S/.test(notes[notes.length - 1])) notes.pop();
+            node.data.note = notes.join('\n');
+        }
+        if (node.children) node.children.forEach(_cleanUp);
+    }
+
+    data.registerProtocol('markdown', module.exports = {
+        fileDescription: 'Markdown/GFM 格式',
+        fileExtension: '.md',
+        mineType: 'text/markdown',
+        dataType: 'text',
+
+        encode: function(json) {
+            return encode(json.root);
+        },
+
+        decode: function(markdown) {
+            return decode(markdown);
+        }
+    });
+});

+ 273 - 0
kityminder-editor/src/protocol/png.js

@@ -0,0 +1,273 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var data = require('../core/data');
+    var Promise = require('../core/promise');
+
+    var DomURL = window.URL || window.webkitURL || window;
+
+    function loadImage(info, callback) {
+        return new Promise(function(resolve, reject) {
+            var image = document.createElement("img");
+            image.onload = function() {
+                resolve({
+                    element: this,
+                    x: info.x,
+                    y: info.y,
+                    width: info.width,
+                    height: info.height
+                });
+            };
+            image.onerror = function(err) {
+                reject(err);
+            };
+
+            image.crossOrigin = 'anonymous';
+            image.src = info.url;
+        });
+    }
+
+    /**
+     * xhrLoadImage: 通过 xhr 加载保存在 BOS 上的图片
+     * @note: BOS 上的 CORS 策略是取 headers 里面的 Origin 字段进行判断
+     *        而通过 image 的 src 的方式是无法传递 origin 的,因此需要通过 xhr 进行
+     */
+    function xhrLoadImage(info, callback) {
+        return Promise(function (resolve, reject) {
+            var xmlHttp = new XMLHttpRequest();
+
+            xmlHttp.open('GET', info.url + '?_=' + Date.now(), true);
+            xmlHttp.responseType = 'blob';
+            xmlHttp.onreadystatechange = function () {
+                if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
+                    var blob = xmlHttp.response;
+
+                    var image = document.createElement('img');
+                    
+                    image.src = DomURL.createObjectURL(blob);                    
+                    image.onload = function () {
+                        DomURL.revokeObjectURL(image.src);
+                        resolve({
+                            element: image,
+                            x: info.x,
+                            y: info.y,
+                            width: info.width,
+                            height: info.height
+                        });
+                    };
+                }
+            };
+
+            xmlHttp.send();
+        });
+    }
+
+    function getSVGInfo(minder) {
+        var paper = minder.getPaper(),
+            paperTransform,
+            domContainer = paper.container,
+            svgXml,
+            svgContainer,
+            svgDom,
+
+            renderContainer = minder.getRenderContainer(),
+            renderBox = renderContainer.getRenderBox(),
+            width = renderBox.width + 1,
+            height = renderBox.height + 1,
+
+            blob, svgUrl, img;
+
+        // 保存原始变换,并且移动到合适的位置
+        paperTransform = paper.shapeNode.getAttribute('transform');
+        paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)');
+        renderContainer.translate(-renderBox.x, -renderBox.y);
+
+        // 获取当前的 XML 代码
+        svgXml = paper.container.innerHTML;
+
+        // 回复原始变换及位置
+        renderContainer.translate(renderBox.x, renderBox.y);
+        paper.shapeNode.setAttribute('transform', paperTransform);
+
+        // 过滤内容
+        svgContainer = document.createElement('div');
+        svgContainer.innerHTML = svgXml;
+        svgDom = svgContainer.querySelector('svg');
+        svgDom.setAttribute('width', renderBox.width + 1);
+        svgDom.setAttribute('height', renderBox.height + 1);
+        svgDom.setAttribute('style', 'font-family: Arial, "Microsoft Yahei","Heiti SC";');
+
+        svgContainer = document.createElement('div');
+        svgContainer.appendChild(svgDom);
+
+        svgXml = svgContainer.innerHTML;
+
+        // Dummy IE
+        svgXml = svgXml.replace(' xmlns="http://www.w3.org/2000/svg" ' +
+            'xmlns:NS1="" NS1:ns1:xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:NS2="" NS2:xmlns:ns1=""', '');
+
+        // svg 含有 &nbsp; 符号导出报错 Entity 'nbsp' not defined ,含有控制字符触发Load Image 会触发报错
+        svgXml = svgXml.replace(/&nbsp;|[\x00-\x1F\x7F-\x9F]/g, "");
+
+        // fix title issue in safari
+        // @ http://stackoverflow.com/questions/30273775/namespace-prefix-ns1-for-href-on-tagelement-is-not-defined-setattributens
+        svgXml = svgXml.replace(/NS\d+:title/gi, 'xlink:title');
+
+        blob = new Blob([svgXml], {
+            type: 'image/svg+xml'
+        });
+
+        svgUrl = DomURL.createObjectURL(blob);
+
+        //svgUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgXml);
+
+        var imagesInfo = [];
+
+        // 遍历取出图片信息
+        traverse(minder.getRoot());
+
+        function traverse(node) {
+            var nodeData = node.data;
+            
+            if (nodeData.image) {
+                minder.renderNode(node);
+                var nodeData = node.data;
+                var imageUrl = nodeData.image;
+                var imageSize = nodeData.imageSize;
+                var imageRenderBox = node.getRenderBox("ImageRenderer", minder.getRenderContainer());
+                var imageInfo = {
+                    url: imageUrl,
+                    width: imageSize.width,
+                    height: imageSize.height,
+                    x: -renderContainer.getBoundaryBox().x + imageRenderBox.x,
+                    y: -renderContainer.getBoundaryBox().y + imageRenderBox.y
+                };
+
+                imagesInfo.push(imageInfo);
+            }
+
+            // 若节点折叠,则直接返回
+            if (nodeData.expandState === 'collapse') {
+                return;
+            }
+
+            var children = node.getChildren();
+            for (var i = 0; i < children.length; i++) {
+                traverse(children[i]);
+            }
+        }
+
+        return {
+            width: width,
+            height: height,
+            dataUrl: svgUrl,
+            xml: svgXml,
+            imagesInfo: imagesInfo
+        };
+    }
+
+
+    function encode(json, minder, option) {
+
+        var resultCallback;
+
+        /* 绘制 PNG 的画布及上下文 */
+        var canvas = document.createElement('canvas');
+        var ctx = canvas.getContext('2d');
+
+        /* 尝试获取背景图片 URL 或背景颜色 */
+        var bgDeclare = minder.getStyle('background').toString();
+        var bgUrl = /url\(\"(.+)\"\)/.exec(bgDeclare);
+        var bgColor = kity.Color.parse(bgDeclare);
+
+        /* 获取 SVG 文件内容 */
+        var svgInfo = getSVGInfo(minder);
+        var width = option && option.width && option.width > svgInfo.width ? option.width : svgInfo.width;
+        var height = option && option.height && option.height > svgInfo.height ? option.height : svgInfo.height;
+        var offsetX = option && option.width && option.width > svgInfo.width ? (option.width - svgInfo.width)/2 : 0;
+        var offsetY = option && option.height && option.height > svgInfo.height ? (option.height - svgInfo.height)/2 : 0;
+        var svgDataUrl = svgInfo.dataUrl;
+        var imagesInfo = svgInfo.imagesInfo;
+
+        /* 画布的填充大小 */
+        var padding = 20;
+
+        canvas.width = width + padding * 2;
+        canvas.height = height + padding * 2;
+
+        function fillBackground(ctx, style) {
+            ctx.save();
+            ctx.fillStyle = style;
+            ctx.fillRect(0, 0, canvas.width, canvas.height);
+            ctx.restore();
+        }
+
+        function drawImage(ctx, image, x, y, width, height) {
+            if (width && height) {
+                ctx.drawImage(image, x + padding, y + padding, width, height);
+            } else {
+                ctx.drawImage(image, x + padding, y + padding);
+            }
+        }
+
+        function generateDataUrl(canvas) {
+            return canvas.toDataURL('image/png');
+        }
+
+        // 加载节点上的图片
+        function loadImages(imagesInfo) {
+            var imagePromises = imagesInfo.map(function(imageInfo) {
+                return xhrLoadImage(imageInfo);
+            });
+
+            return Promise.all(imagePromises);
+        }
+
+        function drawSVG() {
+            var svgData = {url: svgDataUrl};
+
+            return loadImage(svgData).then(function($image) {
+                drawImage(ctx, $image.element, offsetX, offsetY, $image.width, $image.height);
+                return loadImages(imagesInfo);
+            }).then(function($images) {
+                for(var i = 0; i < $images.length; i++) {
+                    drawImage(ctx, $images[i].element, $images[i].x + offsetX, $images[i].y + offsetY, $images[i].width, $images[i].height);
+                }
+
+                DomURL.revokeObjectURL(svgDataUrl);
+                document.body.appendChild(canvas);
+                var pngBase64 = generateDataUrl(canvas);
+                
+                document.body.removeChild(canvas);
+                return pngBase64;
+            }, function(err) {
+                // 这里处理 reject,出错基本上是因为跨域,
+                // 出错后依然导出,只不过没有图片。
+                alert('脑图的节点中包含跨域图片,导出的 png 中节点图片不显示,你可以替换掉这些跨域的图片并重试。');
+                DomURL.revokeObjectURL(svgDataUrl);
+                document.body.appendChild(canvas);
+
+                var pngBase64 = generateDataUrl(canvas);
+                document.body.removeChild(canvas);
+                return pngBase64;
+            });
+        }
+
+        if (bgUrl) {
+            var bgInfo = {url: bgUrl[1]};
+            return loadImage(bgInfo).then(function($image) {
+                fillBackground(ctx, ctx.createPattern($image.element, "repeat"));
+                return drawSVG();
+            });
+        } else {
+            fillBackground(ctx, bgColor.toString());
+            return drawSVG();
+        }
+    }
+    data.registerProtocol("png", module.exports = {
+        fileDescription: "PNG 图片",
+        fileExtension: ".png",
+        mineType: "image/png",
+        dataType: "base64",
+        encode: encode
+    });
+});

+ 243 - 0
kityminder-editor/src/protocol/svg.js

@@ -0,0 +1,243 @@
+define(function(require, exports, module) {
+    var data = require('../core/data');
+
+    /**
+     * 导出svg时删除全部svg元素中的transform
+     * @auth Naixor
+     * @method removeTransform
+     * @param  {[type]}        svgDom [description]
+     * @return {[type]}               [description]
+     */
+     function cleanSVG(svgDom, x, y) {
+             function getTransformToElement(target, source) {
+                 var matrix;
+                 try {
+                     matrix = source.getScreenCTM().inverse();
+                 } catch (e) {
+                     throw new Error("Can not inverse source element' ctm.");
+                 }
+                 return matrix.multiply(target.getScreenCTM());
+             }
+             function dealWithPath(d, dealWithPattern) {
+                 if (!(dealWithPattern instanceof Function)) {
+                     dealWithPattern = function() {};
+                 }
+                 var strArr = [], pattern = [], cache = [];
+                 for (var i = 0, l = d.length; i < l; i++) {
+                     switch (d[i]) {
+                         case "M":
+                         case "L":
+                         case "T":
+                         case "S":
+                         case "A":
+                         case "C":
+                         case "H":
+                         case "V":
+                         case "Q": {
+                             if (cache.length) {
+                                 pattern.push(cache.join(""));
+                                 cache = [];
+                             }
+                             // 脑图的path格式真奇怪...偶尔就给我蹦出来一个"..V123 C..", 那空格几个意思 - -
+                             if (pattern[pattern.length-1] === ",") {
+                                 pattern.pop();
+                             }
+                             if (pattern.length) {
+                                 dealWithPattern(pattern);
+                                 strArr.push(pattern.join(""));
+                                 pattern = [];
+                             }
+                             pattern.push(d[i]);
+                             break;
+                         }
+                         case "Z":
+                         case "z": {
+                             pattern.push(cache.join(""), d[i]);
+                             dealWithPattern(pattern);
+                             strArr.push(pattern.join(""));
+                             cache = [];
+                             pattern = [];
+                             break;
+                         }
+                         case ".":
+                         case "e": {
+                             cache.push(d[i]);
+                             break;
+                         }
+                         case "-": {
+                             if (d[i-1] !== "e") {
+                                 if (cache.length) {
+                                     pattern.push(cache.join(""), ",");
+                                 }
+                                 cache = [];
+                             }
+                             cache.push("-");
+                             break;
+                         }
+                         case " ":
+                         case ",": {
+                             if (cache.length) {
+                                 pattern.push(cache.join(""), ",");
+                                 cache = [];
+                             }
+                             break;
+                         }
+                         default: {
+                             if (/\d/.test(d[i])) {
+                                 cache.push(d[i]);
+                             } else {
+                                 // m a c s q h v l t z情况
+                                 if (cache.length) {
+                                     pattern.push(cache.join(""), d[i]);
+                                     cache = [];
+                                 } else {
+                                     // 脑图的path格式真奇怪...偶尔就给我蹦出来一个"..V123 c..", 那空格几个意思 - -
+                                     if (pattern[pattern.length-1] === ",") {
+                                         pattern.pop();
+                                     }
+                                     pattern.push(d[i]);
+                                 }
+                             }
+                             if (i + 1 === l) {
+                                 if (cache.length) {
+                                     pattern.push(cache.join(""));
+                                 }
+                                 dealWithPattern(pattern);
+                                 strArr.push(pattern.join(""));
+                                 cache = null;
+                                 pattern = null;
+                             }
+                         }
+                     }
+                 }
+                 return strArr.join("");
+             }
+
+             function replaceWithNode(svgNode, parentX, parentY) {
+                 if (!svgNode) {
+                     return;
+                 }
+                 if (svgNode.tagName === "defs") {
+                     return;
+                 }
+                 if (svgNode.getAttribute('fill') === 'transparent') {
+                     svgNode.setAttribute('fill', 'none');
+                 }
+                 if (svgNode.getAttribute('marker-end')) {
+                     svgNode.removeAttribute('marker-end');
+                 }
+                 parentX = parentX || 0;
+                 parentY = parentY || 0;
+                 if (svgNode.getAttribute("transform")) {
+                     var ctm = getTransformToElement(svgNode, svgNode.parentElement);
+                     parentX -= ctm.e;
+                     parentY -= ctm.f;
+                     svgNode.removeAttribute("transform");
+                 }
+                 switch (svgNode.tagName.toLowerCase()) {
+                     case "g":
+                         break;
+                     case "path": {
+                         var d = svgNode.getAttribute("d");
+                         if (d) {
+                             d = dealWithPath(d, function(pattern) {
+                                 switch (pattern[0]) {
+                                     case "V": {
+                                         pattern[1] = +pattern[1] - parentY;
+                                         break;
+                                     }
+                                     case "H": {
+                                         pattern[1] = +pattern[1] - parentX;
+                                         break;
+                                     }
+                                     case "M":
+                                     case "L":
+                                     case "T": {
+                                         pattern[1] = +pattern[1] - parentX;
+                                         pattern[3] = +pattern[3] - parentY;
+                                         break;
+                                     }
+                                     case "Q":
+                                     case "S": {
+                                         pattern[1] = +pattern[1] - parentX;
+                                         pattern[3] = +pattern[3] - parentY;
+                                         pattern[5] = +pattern[5] - parentX;
+                                         pattern[7] = +pattern[7] - parentY;
+                                         break;
+                                     }
+                                     case "A": {
+                                         pattern[11] = +pattern[11] - parentX;
+                                         pattern[13] = +pattern[13] - parentY;
+                                         break;
+                                     }
+                                     case "C": {
+                                         pattern[1] = +pattern[1] - parentX;
+                                         pattern[3] = +pattern[3] - parentY;
+                                         pattern[5] = +pattern[5] - parentX;
+                                         pattern[7] = +pattern[7] - parentY;
+                                         pattern[9] = +pattern[9] - parentX;
+                                         pattern[11] = +pattern[11] - parentY;
+                                     }
+                                 }
+                             });
+                             svgNode.setAttribute("d", d);
+                             svgNode.removeAttribute("transform");
+                         }
+                         return;
+                    }
+                    case "image":
+                    case "text": {
+                        if (parentX && parentY) {
+                            var x = +svgNode.getAttribute("x") || 0, y = +svgNode.getAttribute("y") || 0;
+                            svgNode.setAttribute("x", x - parentX);
+                            svgNode.setAttribute("y", y - parentY);
+                        }
+                        if (svgNode.getAttribute("dominant-baseline")) {
+                            svgNode.removeAttribute("dominant-baseline");
+                            svgNode.setAttribute("dy", ".8em");
+                        }
+                        svgNode.removeAttribute("transform");
+                        return;
+                     }
+                 }
+                 if (svgNode.children) {
+                     for (var i = 0, l = svgNode.children.length; i < l; i++) {
+                         replaceWithNode(svgNode.children[i], parentX, parentY);
+                     }
+                 }
+             }
+             svgDom.style.visibility = "hidden";
+             replaceWithNode(svgDom, x || 0, y || 0);
+             svgDom.style.visibility = "visible";
+         }
+         data.registerProtocol("svg", module.exports = {
+             fileDescription: "SVG 矢量图",
+             fileExtension: ".svg",
+             mineType: "image/svg+xml",
+             dataType: "text",
+             encode: function(json, minder) {
+                 var paper = minder.getPaper(), paperTransform = paper.shapeNode.getAttribute("transform"), svgXml, svgContainer, svgDom, renderContainer = minder.getRenderContainer(), renderBox = renderContainer.getRenderBox(), transform = renderContainer.getTransform(), width = renderBox.width, height = renderBox.height, padding = 20;
+                 paper.shapeNode.setAttribute("transform", "translate(0.5, 0.5)");
+                 svgXml = paper.container.innerHTML;
+                 paper.shapeNode.setAttribute("transform", paperTransform);
+                 svgContainer = document.createElement("div");
+                 document.body.appendChild(svgContainer);
+                 svgContainer.innerHTML = svgXml;
+                 svgDom = svgContainer.querySelector("svg");
+                 svgDom.setAttribute("width", width + padding * 2 | 0);
+                 svgDom.setAttribute("height", height + padding * 2 | 0);
+                 svgDom.setAttribute("style", "background: " + minder.getStyle("background"));//"font-family: Arial, Microsoft Yahei, Heiti SC; " +
+                 svgDom.setAttribute("viewBox", [ 0, 0, width + padding * 2 | 0, height + padding * 2 | 0 ].join(" "));
+                 tempSvgContainer = document.createElement("div");
+                 cleanSVG(svgDom, renderBox.x - padding | 0, renderBox.y - padding | 0);
+                 document.body.removeChild(svgContainer);
+                 tempSvgContainer.appendChild(svgDom);
+                 // need a xml with width and height
+                 svgXml = tempSvgContainer.innerHTML;
+                 // svg 含有 &nbsp; 符号导出报错 Entity 'nbsp' not defined
+                 svgXml = svgXml.replace(/&nbsp;/g, "&#xa0;");
+                 // svg 含有 &nbsp; 符号导出报错 Entity 'nbsp' not defined
+                 return svgXml;
+             }
+         });
+});

+ 237 - 0
kityminder-editor/src/protocol/text.js

@@ -0,0 +1,237 @@
+define(function(require, exports, module) {
+    var data = require('../core/data');
+    var Browser = require('../core/kity').Browser;
+
+    /**
+     * @Desc: 增加对不容浏览器下节点中文本\t匹配的处理,不同浏览器下\t无法正确匹配,导致无法使用TAB来批量导入节点
+     * @Editor: Naixor
+     * @Date: 2015.9.17
+     */
+    var LINE_ENDING = '\r',
+        LINE_ENDING_SPLITER = /\r\n|\r|\n/,
+        TAB_CHAR = (function(Browser) {
+            if (Browser.gecko) {
+                return {
+                    REGEXP: new RegExp('^(\t|'+ String.fromCharCode(160,160,32,160) +')'),
+                    DELETE: new RegExp('^(\t|'+ String.fromCharCode(160,160,32,160) +')+')
+                }
+            } else if (Browser.ie || Browser.edge) {
+                // ie系列和edge比较特别,\t在div中会被直接转义成SPACE故只好使用SPACE来做处理
+                return {
+                    REGEXP: new RegExp('^('+ String.fromCharCode(32) +'|'+ String.fromCharCode(160) +')'),
+                    DELETE: new RegExp('^('+ String.fromCharCode(32) +'|'+ String.fromCharCode(160) +')+')
+                }
+            } else {
+                return {
+                    REGEXP: /^(\t|\x20{4})/,
+                    DELETE: /^(\t|\x20{4})+/
+                }
+            }
+        })(Browser);
+
+    function repeat(s, n) {
+        var result = '';
+        while (n--) result += s;
+        return result;
+    }
+
+    /**
+     * 对节点text中的换行符进行处理
+     * @method encodeWrap
+     * @param  {String}   nodeText MinderNode.data.text
+     * @return {String}            \n -> '\n'; \\n -> '\\n'
+     */
+    function encodeWrap(nodeText) {
+        if (!nodeText) {
+            return '';
+        }
+        var textArr = [],
+            WRAP_TEXT = ['\\', 'n'];
+        for (var i = 0, j = 0, l = nodeText.length; i < l; i++) {
+            if (nodeText[i] === '\n' || nodeText[i] === '\r') {
+                textArr.push('\\n');
+                j = 0;
+                continue;
+            }
+            if (nodeText[i] === WRAP_TEXT[j]) {
+                j++;
+                if (j === 2) {
+                    j = 0;
+                    textArr.push('\\\\n');
+                }
+                continue;
+            }
+            switch (j) {
+                case 0: {
+                    textArr.push(nodeText[i]);
+                    break;
+                }
+                case 1: {
+                    textArr.push(nodeText[i-1], nodeText[i]);
+                }
+            }
+            j = 0;
+        }
+        return textArr.join('');
+    }
+
+    /**
+     * 将文本内容中的'\n'和'\\n'分别转换成\n和\\n
+     * @method decodeWrap
+     * @param  {[type]}   text [description]
+     * @return {[type]}        [description]
+     */
+    function decodeWrap(text) {
+        if (!text) {
+            return '';
+        }
+        var textArr = [],
+            WRAP_TEXT = ['\\', '\\', 'n'];
+        for (var i = 0, j = 0, l = text.length; i < l; i++) {
+            if (text[i] === WRAP_TEXT[j]) {
+                j++;
+                if (j === 3) {
+                    j = 0;
+                    textArr.push('\\n');
+                }
+                continue;
+            }
+            switch (j) {
+                case 0: {
+                    textArr.push(text[i]);
+                    j = 0;
+                    break;
+                }
+                case 1: {
+                    if (text[i] === 'n') {
+                        textArr.push('\n');
+                    } else {
+                        textArr.push(text[i-1], text[i]);
+                    }
+                    j = 0;
+                    break;
+                }
+                case 2: {
+                    textArr.push(text[i-2]);
+                    if (text[i] !== '\\') {
+                        j = 0;
+                        textArr.push(text[i-1], text[i]);
+                    }
+                    break;
+                }
+            }
+        }
+        return textArr.join('');
+    }
+
+    function encode(json, level) {
+        var local = '';
+        level = level || 0;
+        local += repeat('\t', level);
+        local += encodeWrap(json.data.text) + LINE_ENDING;
+        if (json.children) {
+            json.children.forEach(function(child) {
+                local += encode(child, level + 1);
+            });
+        }
+        return local;
+    }
+
+    function isEmpty(line) {
+        return !/\S/.test(line);
+    }
+
+    function getLevel(line) {
+        var level = 0;
+        while (TAB_CHAR.REGEXP.test(line)) {
+            line = line.replace(TAB_CHAR.REGEXP, '');
+            level++;
+        }
+
+        return level;
+    }
+
+    function getNode(line) {
+        return {
+            data: {
+                text: decodeWrap(line.replace(TAB_CHAR.DELETE, ""))
+            }
+        };
+    }
+
+    function decode(local) {
+        var json,
+            parentMap = {},
+            lines = local.split(LINE_ENDING_SPLITER),
+            line, level, node;
+
+        function addChild(parent, child) {
+            var children = parent.children || (parent.children = []);
+            children.push(child);
+        }
+
+        for (var i = 0; i < lines.length; i++) {
+            line = lines[i];
+            if (isEmpty(line)) continue;
+
+            level = getLevel(line);
+            node = getNode(line);
+
+            if (level === 0) {
+                if (json) {
+                    throw new Error('Invalid local format');
+                }
+                json = node;
+            } else {
+                if (!parentMap[level - 1]) {
+                    throw new Error('Invalid local format');
+                }
+                addChild(parentMap[level - 1], node);
+            }
+            parentMap[level] = node;
+        }
+        return json;
+    }
+
+    /**
+     * @Desc: 增加一个将当前选中节点转换成text的方法
+     * @Editor: Naixor
+     * @Date: 2015.9.21
+     */
+    function Node2Text(node) {
+        function exportNode(node) {
+            var exported = {};
+            exported.data = node.getData();
+            var childNodes = node.getChildren();
+            exported.children = [];
+            for (var i = 0; i < childNodes.length; i++) {
+                exported.children.push(exportNode(childNodes[i]));
+            }
+            return exported;
+        }
+        if (!node) return;
+        if (/^\s*$/.test(node.data.text)) {
+            node.data.text = "分支主题";
+        }
+        return encode(exportNode(node));
+    }
+
+    data.registerProtocol('text', module.exports = {
+        fileDescription: '大纲文本',
+        fileExtension: '.txt',
+        dataType: 'text',
+        mineType: 'text/plain',
+
+        encode: function(json) {
+            return encode(json.root, 0);
+        },
+
+        decode: function(local) {
+            return decode(local);
+        },
+
+        Node2Text: function(node) {
+            return Node2Text(node);
+        }
+    });
+});

+ 38 - 0
kityminder-editor/src/template/default.js

@@ -0,0 +1,38 @@
+/**
+ * @fileOverview
+ *
+ * 默认模板 - 脑图模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var template = require('../core/template');
+
+    template.register('default', {
+
+        getLayout: function(node) {
+
+            if (node.getData('layout')) return node.getData('layout');
+
+            var level = node.getLevel();
+
+            // 根节点
+            if (level === 0) {
+                return 'mind';
+            }
+
+            // 一级节点
+            if (level === 1) {
+                return node.getLayoutPointPreview().x > 0 ? 'right': 'left';
+            }
+
+            return node.parent.getLayout();
+        },
+
+        getConnect: function(node) {
+            if (node.getLevel() == 1) return 'arc';
+            return 'under';
+        }
+    });
+});

+ 28 - 0
kityminder-editor/src/template/filetree.js

@@ -0,0 +1,28 @@
+/**
+ * @fileOverview
+ *
+ * 文件夹模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var template = require('../core/template');
+
+    template.register('filetree', {
+
+        getLayout: function(node) {
+            if (node.getData('layout')) return node.getData('layout');
+            if (node.isRoot()) return 'bottom';
+
+            return 'filetree-down';
+        },
+
+        getConnect: function(node) {
+            if (node.getLevel() == 1) {
+                return 'poly';
+            }
+            return 'l';
+        }
+    });
+});

+ 41 - 0
kityminder-editor/src/template/fish-bone.js

@@ -0,0 +1,41 @@
+/**
+ * @fileOverview
+ *
+ * 默认模板 - 鱼骨头模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var template = require('../core/template');
+
+    template.register('fish-bone', {
+
+        getLayout: function(node) {
+
+            if (node.getData('layout')) return node.getData('layout');
+
+            var level = node.getLevel();
+
+            // 根节点
+            if (level === 0) {
+                return 'fish-bone-master';
+            }
+
+            // 一级节点
+            if (level === 1) {
+                return 'fish-bone-slave';
+            }
+
+            return node.getLayoutPointPreview().y > 0 ? 'filetree-up': 'filetree-down';
+        },
+
+        getConnect: function(node) {
+            switch (node.getLevel()) {
+                case 1: return 'fish-bone-master';
+                case 2: return 'line';
+                default: return 'l';
+            }
+        }
+    });
+});

+ 23 - 0
kityminder-editor/src/template/right.js

@@ -0,0 +1,23 @@
+/**
+ * @fileOverview
+ *
+ * 往右布局结构模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var template = require('../core/template');
+
+    template.register('right', {
+
+        getLayout: function(node) {
+            return node.getData('layout') || 'right';
+        },
+
+        getConnect: function(node) {
+            if (node.getLevel() == 1) return 'arc';
+            return 'bezier';
+        }
+    });
+});

+ 22 - 0
kityminder-editor/src/template/structure.js

@@ -0,0 +1,22 @@
+/**
+ * @fileOverview
+ *
+ * 组织结构图模板
+ *
+ * @author: techird
+ * @copyright: Baidu FEX, 2014
+ */
+define(function(require, exports, module) {
+    var template = require('../core/template');
+
+    template.register('structure', {
+
+        getLayout: function(node) {
+            return node.getData('layout') || 'bottom';
+        },
+
+        getConnect: function(node) {
+            return 'poly';
+        }
+    });
+});

+ 29 - 0
kityminder-editor/src/template/tianpan.js

@@ -0,0 +1,29 @@
+/**
+ * @fileOverview
+ *
+ * 天盘模板
+ *
+ * @author: along
+ * @copyright: bpd729@163.com, 2015
+ */
+define(function(require, exports, module) {
+    var template = require('../core/template');
+
+    template.register('tianpan', {
+        getLayout: function (node) {
+            if (node.getData('layout')) return node.getData('layout');
+            var level = node.getLevel();
+
+            // 根节点
+            if (level === 0) {
+                return 'tianpan';
+            }
+
+            return node.parent.getLayout();
+        },
+
+        getConnect: function (node) {
+            return 'arc_tp';
+        }
+    });
+});

+ 66 - 0
kityminder-editor/src/theme/default.js

@@ -0,0 +1,66 @@
+define(function(require, exports, module) {
+    var theme = require('../core/theme');
+
+    ['classic', 'classic-compact'].forEach(function(name) {
+        var compact = name == 'classic-compact';
+
+        /* jscs:disable maximumLineLength */
+        theme.register(name, {
+            'background': '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+
+            'root-color': '#430',
+            'root-background': '#e9df98',
+            'root-stroke': '#e9df98',
+            'root-font-size': 24,
+            'root-padding': compact ? [10, 25] : [15, 25],
+            'root-margin': compact ? [15, 25] : [30, 100],
+            'root-radius': 30,
+            'root-space': 10,
+            'root-shadow': 'rgba(0, 0, 0, .25)',
+
+            'main-color': '#333',
+            'main-background': '#a4c5c0',
+            'main-stroke': '#a4c5c0',
+            'main-font-size': 16,
+            'main-padding': compact ? [5, 15] : [6, 20],
+            'main-margin': compact ? [5, 10] : 20,
+            'main-radius': 10,
+            'main-space': 5,
+            'main-shadow': 'rgba(0, 0, 0, .25)',
+
+            'sub-color': 'white',
+            'sub-background': 'transparent',
+            'sub-stroke': 'none',
+            'sub-font-size': 12,
+            'sub-padding': [5, 10],
+            'sub-margin': compact ? [5, 10] : [15, 20],
+            'sub-tree-margin': 30,
+            'sub-radius': 5,
+            'sub-space': 5,
+
+            'connect-color': 'white',
+            'connect-width': 2,
+            'main-connect-width': 3,
+            'connect-radius': 5,
+
+            'selected-background': 'rgb(254, 219, 0)',
+            'selected-stroke': 'rgb(254, 219, 0)',
+            'selected-color': 'black',
+
+            'marquee-background': 'rgba(255,255,255,.3)',
+            'marquee-stroke': 'white',
+
+            'drop-hint-color': 'yellow',
+            'sub-drop-hint-width': 2,
+            'main-drop-hint-width': 4,
+            'root-drop-hint-width': 4,
+
+            'order-hint-area-color': 'rgba(0, 255, 0, .5)',
+            'order-hint-path-color': '#0f0',
+            'order-hint-path-width': 1,
+
+            'text-selection-color': 'rgb(27,171,255)',
+            'line-height':1.5
+        });
+    });
+});

+ 57 - 0
kityminder-editor/src/theme/fish.js

@@ -0,0 +1,57 @@
+define(function(require, exports, module) {
+    var theme = require('../core/theme');
+
+    theme.register('fish', {
+        'background': '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+
+        'root-color': '#430',
+        'root-background': '#e9df98',
+        'root-stroke': '#e9df98',
+        'root-font-size': 24,
+        'root-padding': [35, 35],
+        'root-margin': 30,
+        'root-radius': 100,
+        'root-space': 10,
+        'root-shadow': 'rgba(0, 0, 0, .25)',
+
+        'main-color': '#333',
+        'main-background': '#a4c5c0',
+        'main-stroke': '#a4c5c0',
+        'main-font-size': 16,
+        'main-padding': [6, 20],
+        'main-margin': [20, 20],
+        'main-radius': 5,
+        'main-space': 5,
+        'main-shadow': 'rgba(0, 0, 0, .25)',
+
+        'sub-color': 'black',
+        'sub-background': 'white',
+        'sub-stroke': 'white',
+        'sub-font-size': 12,
+        'sub-padding': [5, 10],
+        'sub-margin': [10],
+        'sub-radius': 5,
+        'sub-space': 5,
+
+        'connect-color': 'white',
+        'connect-width': 3,
+        'main-connect-width': 3,
+        'connect-radius': 5,
+
+        'selected-background': 'rgb(254, 219, 0)',
+        'selected-stroke': 'rgb(254, 219, 0)',
+
+        'marquee-background': 'rgba(255,255,255,.3)',
+        'marquee-stroke': 'white',
+
+        'drop-hint-color': 'yellow',
+        'drop-hint-width': 4,
+
+        'order-hint-area-color': 'rgba(0, 255, 0, .5)',
+        'order-hint-path-color': '#0f0',
+        'order-hint-path-width': 1,
+
+        'text-selection-color': 'rgb(27,171,255)',
+        'line-height':1.5
+    });
+});

+ 78 - 0
kityminder-editor/src/theme/fresh.js

@@ -0,0 +1,78 @@
+define(function(require, exports, module) {
+    var kity = require('../core/kity');
+    var theme = require('../core/theme');
+
+    function hsl(h, s, l) {
+        return kity.Color.createHSL(h, s, l);
+    }
+
+    function generate(h, compat) {
+        return {
+            'background': '#fbfbfb',
+
+            'root-color': 'white',
+            'root-background': hsl(h, 37, 60),
+            'root-stroke': hsl(h, 37, 60),
+            'root-font-size': 16,
+            'root-padding': compat ? [6, 12] : [12, 24],
+            'root-margin': compat ? 10 : [30, 100],
+            'root-radius': 5,
+            'root-space': 10,
+
+            'main-color': 'black',
+            'main-background': hsl(h, 33, 95),
+            'main-stroke': hsl(h, 37, 60),
+            'main-stroke-width': 1,
+            'main-font-size': 14,
+            'main-padding': [6, 20],
+            'main-margin': compat ? 8 : 20,
+            'main-radius': 3,
+            'main-space': 5,
+
+            'sub-color': 'black',
+            'sub-background': 'transparent',
+            'sub-stroke': 'none',
+            'sub-font-size': 12,
+            'sub-padding': compat ? [3, 5] : [5, 10],
+            'sub-margin': compat ? [4, 8] : [15, 20],
+            'sub-radius': 5,
+            'sub-space': 5,
+
+            'connect-color': hsl(h, 37, 60),
+            'connect-width': 1,
+            'connect-radius': 5,
+
+            'selected-stroke': hsl(h, 26, 30),
+            'selected-stroke-width': '3',
+            'blur-selected-stroke': hsl(h, 10, 60),
+
+            'marquee-background': hsl(h, 100, 80).set('a', 0.1),
+            'marquee-stroke': hsl(h, 37, 60),
+
+            'drop-hint-color': hsl(h, 26, 35),
+            'drop-hint-width': 5,
+
+            'order-hint-area-color': hsl(h, 100, 30).set('a', 0.5),
+            'order-hint-path-color': hsl(h, 100, 25),
+            'order-hint-path-width': 1,
+
+            'text-selection-color': hsl(h, 100, 20),
+            'line-height':1.5
+        };
+    }
+
+    var plans = {
+        red: 0,
+        soil: 25,
+        green: 122,
+        blue: 204,
+        purple: 246,
+        pink: 334
+    };
+    var name;
+    for (name in plans) {
+        theme.register('fresh-' + name, generate(plans[name]));
+        theme.register('fresh-' + name + '-compat', generate(plans[name], true));
+    }
+
+});

+ 62 - 0
kityminder-editor/src/theme/snow.js

@@ -0,0 +1,62 @@
+define(function(require, exports, module) {
+    var theme = require('../core/theme');
+
+    ['snow', 'snow-compact'].forEach(function(name) {
+        var compact = name == 'snow-compact';
+
+        /* jscs:disable maximumLineLength */
+        theme.register(name, {
+            'background': '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+
+            'root-color': '#430',
+            'root-background': '#e9df98',
+            'root-stroke': '#e9df98',
+            'root-font-size': 24,
+            'root-padding': compact ? [5, 10] : [15, 25],
+            'root-margin': compact ? 15 : 30,
+            'root-radius': 5,
+            'root-space': 10,
+            'root-shadow': 'rgba(0, 0, 0, .25)',
+
+            'main-color': '#333',
+            'main-background': '#a4c5c0',
+            'main-stroke': '#a4c5c0',
+            'main-font-size': 16,
+            'main-padding': compact ? [4, 10] : [6, 20],
+            'main-margin': compact ? [5, 10] : [20, 40],
+            'main-radius': 5,
+            'main-space': 5,
+            'main-shadow': 'rgba(0, 0, 0, .25)',
+
+            'sub-color': 'black',
+            'sub-background': 'white',
+            'sub-stroke': 'white',
+            'sub-font-size': 12,
+            'sub-padding': [5, 10],
+            'sub-margin': compact ? [5, 10] : [10, 20],
+            'sub-radius': 5,
+            'sub-space': 5,
+
+            'connect-color': 'white',
+            'connect-width': 2,
+            'main-connect-width': 3,
+            'connect-radius': 5,
+
+            'selected-background': 'rgb(254, 219, 0)',
+            'selected-stroke': 'rgb(254, 219, 0)',
+
+            'marquee-background': 'rgba(255,255,255,.3)',
+            'marquee-stroke': 'white',
+
+            'drop-hint-color': 'yellow',
+            'drop-hint-width': 4,
+
+            'order-hint-area-color': 'rgba(0, 255, 0, .5)',
+            'order-hint-path-color': '#0f0',
+            'order-hint-path-width': 1,
+
+            'text-selection-color': 'rgb(27,171,255)',
+            'line-height':1.5
+        });
+    });
+});

+ 74 - 0
kityminder-editor/src/theme/tianpan.js

@@ -0,0 +1,74 @@
+define(function(require, exports, module) {
+    var theme = require('../core/theme');
+
+    ['tianpan', 'tianpan-compact'].forEach(function (name) {
+        var compact = name == 'tianpan-compact';
+
+        theme.register(name, {
+            'background': '#3A4144 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowQzg5QTQ0NDhENzgxMUUzOENGREE4QTg0RDgzRTZDNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowQzg5QTQ0NThENzgxMUUzOENGREE4QTg0RDgzRTZDNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkMwOEQ1NDRGOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkMwOEQ1NDUwOEQ3NzExRTM4Q0ZEQThBODREODNFNkM3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+e9P33AAAACVJREFUeNpisXJ0YUACTAyoAMr/+eM7EGGRZ4FQ7BycEAZAgAEAHbEGtkoQm/wAAAAASUVORK5CYII=") repeat',
+
+            'root-color': '#430',
+            'root-background': '#e9df98',
+            'root-stroke': '#e9df98',
+            'root-font-size': 25,
+            'root-padding': compact ? 15 : 20,
+            'root-margin': compact ? [15, 25] : 100,
+            'root-radius': 30,
+            'root-space': 10,
+            'root-shadow': 'rgba(0, 0, 0, .25)',
+            'root-shape': 'circle',
+
+            'main-color': '#333',
+            'main-background': '#a4c5c0',
+            'main-stroke': '#a4c5c0',
+            'main-font-size': 15,
+            'main-padding': compact ? 10 : 12,
+            'main-margin': compact ? 10 : 12,
+            'main-radius': 10,
+            'main-space': 5,
+            'main-shadow': 'rgba(0, 0, 0, .25)',
+            'main-shape': 'circle',
+
+            'sub-color': '#333',
+            'sub-background': '#99ca6a',
+            'sub-stroke': '#a4c5c0',
+            'sub-font-size': 13,
+            'sub-padding': 5,
+            'sub-margin': compact ? 6 : 10,
+            'sub-tree-margin': 30,
+            'sub-radius': 5,
+            'sub-space': 5,
+            'sub-shadow': 'rgba(0, 0, 0, .25)',
+            'sub-shape': 'circle',
+
+            'connect-color': 'white',
+            'connect-width': 2,
+            'main-connect-width': 3,
+            'connect-radius': 5,
+
+            'selected-background': 'rgb(254, 219, 0)',
+            'selected-stroke': 'rgb(254, 219, 0)',
+            'selected-color': 'black',
+
+            'marquee-background': 'rgba(255,255,255,.3)',
+            'marquee-stroke': 'white',
+
+            'drop-hint-color': 'yellow',
+            'sub-drop-hint-width': 2,
+            'main-drop-hint-width': 4,
+            'root-drop-hint-width': 4,
+
+            'order-hint-area-color': 'rgba(0, 255, 0, .5)',
+            'order-hint-path-color': '#0f0',
+            'order-hint-path-width': 1,
+
+            'text-selection-color': 'rgb(27,171,255)',
+            'line-height': 1.4
+        });
+    });
+});
+
+
+
+
+

Неке датотеке нису приказане због велике количине промена