├── .babelrc ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── README.md ├── bower.json ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── src ├── App.vue ├── assets │ ├── iconpriority.png │ ├── iconprogress.png │ ├── icons.png │ ├── logo.png │ └── mold.png ├── components │ ├── Hello.vue │ ├── editor.vue │ ├── footer.vue │ ├── header.vue │ ├── main_editor │ │ ├── main_editor.vue │ │ └── navigator.vue │ └── top_menu │ │ ├── edit_menu │ │ ├── attachment.vue │ │ ├── edit_del.vue │ │ ├── edit_menu.vue │ │ ├── insert_box.vue │ │ ├── label_box.vue │ │ ├── move_box.vue │ │ ├── progress_box.vue │ │ ├── sequence_box.vue │ │ └── undo_redo.vue │ │ ├── select_menu │ │ ├── expand.vue │ │ ├── select_menu.vue │ │ └── selection.vue │ │ └── view_menu │ │ ├── arrange.vue │ │ ├── font_operation.vue │ │ ├── mold.vue │ │ ├── style_operation.vue │ │ ├── theme.vue │ │ └── view_menu.vue ├── main.js ├── router │ └── index.js ├── script │ ├── editor.js │ ├── expose-editor.js │ ├── hotbox.js │ ├── lang.js │ ├── minder.js │ ├── runtime │ │ ├── clipboard-mimetype.js │ │ ├── clipboard.js │ │ ├── container.js │ │ ├── drag.js │ │ ├── fsm.js │ │ ├── history.js │ │ ├── hotbox.js │ │ ├── input.js │ │ ├── jumping.js │ │ ├── minder.js │ │ ├── node.js │ │ ├── priority.js │ │ ├── progress.js │ │ └── receiver.js │ └── tool │ │ ├── debug.js │ │ ├── format.js │ │ ├── innertext.js │ │ ├── jsondiff.js │ │ ├── key.js │ │ └── keymap.js ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ ├── mutations.js │ └── state.js └── style │ ├── dropdown-list.scss │ ├── editor.scss │ ├── header.scss │ ├── mixin.scss │ ├── navigator.scss │ └── normalize.css ├── static └── .gitkeep ├── test ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js └── unit │ ├── .eslintrc │ ├── index.js │ ├── karma.conf.js │ └── specs │ └── Hello.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | test/unit/coverage 7 | test/e2e/reports 8 | selenium-debug.log 9 | bower_components/ 10 | true/ 11 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mind_map 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | ### 推荐使用[yarn](https://yarnpkg.com) 7 | ```bash 8 | npm i -g yarn 9 | ``` 10 | 11 | ``` bash 12 | # install bower dependencies 13 | bower install 14 | 15 | #以下可用npm代替yarn 16 | # install npm dependencies 17 | yarn install 18 | 19 | # serve with hot reload at localhost:8080 20 | yarn run dev 21 | 22 | # build for production with minification 23 | yarn run build 24 | 25 | # build for production and view the bundle analyzer report 26 | yarn run build --report 27 | 28 | # run unit tests 29 | yarn run unit 30 | 31 | # run e2e tests 32 | yarn run e2e 33 | 34 | # run all tests 35 | yarn test 36 | ``` 37 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mind_map", 3 | "description": "A Vue.js project", 4 | "main": "", 5 | "authors": [ 6 | "LingRay " 7 | ], 8 | "license": "", 9 | "homepage": "", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "hotbox": "^1.0.14", 19 | "kityminder-core": "^1.4.36", 20 | "kity": "^2.0.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = process.env.NODE_ENV === 'testing' 14 | ? require('./webpack.prod.conf') 15 | : require('./webpack.dev.conf') 16 | 17 | // default port where dev server listens for incoming traffic 18 | var port = process.env.PORT || config.dev.port 19 | // automatically open browser, if not set will be false 20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 21 | // Define HTTP proxies to your custom API backend 22 | // https://github.com/chimurai/http-proxy-middleware 23 | var proxyTable = config.dev.proxyTable 24 | 25 | var app = express() 26 | var compiler = webpack(webpackConfig) 27 | 28 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 29 | publicPath: webpackConfig.output.publicPath, 30 | quiet: true 31 | }) 32 | 33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 34 | log: () => {} 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | compiler.plugin('compilation', function (compilation) { 38 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 39 | hotMiddleware.publish({ action: 'reload' }) 40 | cb() 41 | }) 42 | }) 43 | 44 | // proxy api requests 45 | Object.keys(proxyTable).forEach(function (context) { 46 | var options = proxyTable[context] 47 | if (typeof options === 'string') { 48 | options = { target: options } 49 | } 50 | app.use(proxyMiddleware(options.filter || context, options)) 51 | }) 52 | 53 | // handle fallback for HTML5 history API 54 | app.use(require('connect-history-api-fallback')()) 55 | 56 | // serve webpack bundle output 57 | app.use(devMiddleware) 58 | 59 | // enable hot-reload and state-preserving 60 | // compilation error display 61 | app.use(hotMiddleware) 62 | 63 | // serve pure static assets 64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 65 | app.use(staticPath, express.static('./static')) 66 | 67 | var uri = 'http://localhost:' + port 68 | 69 | devMiddleware.waitUntilValid(function () { 70 | console.log('> Listening at ' + uri + '\n') 71 | }) 72 | 73 | module.exports = app.listen(port, function (err) { 74 | if (err) { 75 | console.log(err) 76 | return 77 | } 78 | 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | var webpack = require("webpack") 6 | 7 | function resolve(dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | // 'bower_codemirror': './bower_components/codemirror/lib/codemirror.js', 14 | // 'bower_codemirror_xml.js': './bower_components/codemirror/mode/xml/xml.js', 15 | // 'bower_codemirror_javascript': './bower_components/codemirror/mode/javascript/javascript.js', 16 | // 'bower_codemirror_cssjs': './bower_components/codemirror/mode/css/css.js', 17 | // 'bower_codemirror_htmlmixed': './bower_components/codemirror/mode/htmlmixed/htmlmixed.js', 18 | // 'bower_codemirror_markdown': './bower_components/codemirror/mode/markdown/markdown.js', 19 | // 'bower_codemirror_overlay': './bower_components/codemirror/addon/mode/overlay.js', 20 | // 'bower_codemirror_gfm': './bower_components/codemirror/mode/gfm/gfm.js', 21 | // // 'bower_angular-ui-codemirror':'./bower_components/angular-ui-codemirror/ui-codemirror.js', 22 | // 'bower_marked': './bower_components/marked/lib/marked.js', 23 | // 'bower_kity': './bower_components/kity/dist/kity.js', 24 | // 'bower_hotbox': './bower_components/hotbox/hotbox.js', 25 | // 'bower_jsondiff': './bower_components/json-diff/json-diff.js', 26 | // 'bower_kityminder_core': './bower_components/kityminder-core/dist/kityminder.core.js', 27 | // 'bower_color_picker':'./bower_components/color-picker/dist/color-picker.js', 28 | 'app': './src/main.js', 29 | // 'runtimejs': './src/script/editor.js' 30 | }, 31 | output: { 32 | path: config.build.assetsRoot, 33 | filename: '[name].js', 34 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath 35 | }, 36 | resolve: { 37 | extensions: [ 38 | '.js', '.vue', '.json' 39 | ], 40 | alias: { 41 | 'vue$': 'vue/dist/vue.esm.js', 42 | '@': resolve('src') 43 | } 44 | }, 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.vue$/, 49 | loader: 'vue-loader', 50 | options: vueLoaderConfig 51 | }, { 52 | test: /\.js$/, 53 | loader: 'babel-loader', 54 | include: [resolve('src'), resolve('test')] 55 | }, { 56 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 57 | loader: 'url-loader', 58 | query: { 59 | limit: 10000, 60 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 61 | } 62 | }, { 63 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 64 | loader: 'url-loader', 65 | query: { 66 | limit: 10000, 67 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 68 | } 69 | } 70 | ] 71 | }, 72 | plugins: [ 73 | new webpack.optimize.CommonsChunkPlugin('common.js'), 74 | new webpack.ProvidePlugin({jQuery: "jquery", $: "jquery"}) 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = process.env.NODE_ENV === 'testing' 13 | ? require('../config/test.env') 14 | : config.build.env 15 | 16 | var webpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true 21 | }) 22 | }, 23 | devtool: config.build.productionSourceMap ? '#source-map' : false, 24 | output: { 25 | path: config.build.assetsRoot, 26 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 27 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin(), 47 | // generate dist index.html with correct asset hash for caching. 48 | // you can customize output by editing /index.html 49 | // see https://github.com/ampedandwired/html-webpack-plugin 50 | new HtmlWebpackPlugin({ 51 | filename: process.env.NODE_ENV === 'testing' 52 | ? 'index.html' 53 | : config.build.index, 54 | template: 'index.html', 55 | inject: true, 56 | minify: { 57 | removeComments: true, 58 | collapseWhitespace: true, 59 | removeAttributeQuotes: true 60 | // more options: 61 | // https://github.com/kangax/html-minifier#options-quick-reference 62 | }, 63 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 64 | chunksSortMode: 'dependency' 65 | }), 66 | // split vendor js into its own file 67 | new webpack.optimize.CommonsChunkPlugin({ 68 | name: 'vendor', 69 | minChunks: function (module, count) { 70 | // any required modules inside node_modules are extracted to vendor 71 | return ( 72 | module.resource && 73 | /\.js$/.test(module.resource) && 74 | module.resource.indexOf( 75 | path.join(__dirname, '../node_modules') 76 | ) === 0 77 | ) 78 | } 79 | }), 80 | // extract webpack runtime and module manifest to its own file in order to 81 | // prevent vendor hash from being updated whenever app bundle is updated 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'manifest', 84 | chunks: ['vendor'] 85 | }), 86 | // copy custom static assets 87 | new CopyWebpackPlugin([ 88 | { 89 | from: path.resolve(__dirname, '../static'), 90 | to: config.build.assetsSubDirectory, 91 | ignore: ['.*'] 92 | } 93 | ]) 94 | ] 95 | }) 96 | 97 | if (config.build.productionGzip) { 98 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 99 | 100 | webpackConfig.plugins.push( 101 | new CompressionWebpackPlugin({ 102 | asset: '[path].gz[query]', 103 | algorithm: 'gzip', 104 | test: new RegExp( 105 | '\\.(' + 106 | config.build.productionGzipExtensions.join('|') + 107 | ')$' 108 | ), 109 | threshold: 10240, 110 | minRatio: 0.8 111 | }) 112 | ) 113 | } 114 | 115 | if (config.build.bundleAnalyzerReport) { 116 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 117 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 118 | } 119 | 120 | module.exports = webpackConfig 121 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | plugins: [ 15 | new webpack.DefinePlugin({ 16 | 'process.env': require('../config/test.env') 17 | }) 18 | ] 19 | }) 20 | 21 | // no need for app entry during tests 22 | delete webpackConfig.entry 23 | 24 | module.exports = webpackConfig 25 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: false, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mind_map 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mind_map", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "LingRay ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run unit && npm run e2e" 13 | }, 14 | "dependencies": { 15 | "codemirror": "^5.24.2", 16 | "css-loader": "^0.27.3", 17 | "element-ui": "^1.2.4", 18 | "jquery": "^3.2.0", 19 | "marked": "^0.3.6", 20 | "node-sass": "^4.5.3", 21 | "vue": "^2.2.4", 22 | "vue-router": "^2.2.4", 23 | "vuex": "^2.2.1", 24 | "yarn": "^0.21.3" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^6.7.2", 28 | "babel-core": "^6.22.1", 29 | "babel-loader": "^6.2.10", 30 | "babel-plugin-istanbul": "^3.1.2", 31 | "babel-plugin-transform-runtime": "^6.22.0", 32 | "babel-preset-env": "^1.2.1", 33 | "babel-preset-stage-2": "^6.22.0", 34 | "babel-register": "^6.22.0", 35 | "chai": "^3.5.0", 36 | "chalk": "^1.1.3", 37 | "chromedriver": "^2.27.2", 38 | "connect-history-api-fallback": "^1.3.0", 39 | "copy-webpack-plugin": "^4.0.1", 40 | "cross-env": "^3.1.4", 41 | "cross-spawn": "^5.0.1", 42 | "css-loader": "^0.27.3", 43 | "eventsource-polyfill": "^0.9.6", 44 | "express": "^4.14.1", 45 | "extract-text-webpack-plugin": "^2.0.0", 46 | "file-loader": "^0.10.0", 47 | "friendly-errors-webpack-plugin": "^1.1.3", 48 | "function-bind": "^1.1.0", 49 | "html-webpack-plugin": "^2.28.0", 50 | "http-proxy-middleware": "^0.17.3", 51 | "inject-loader": "^2.0.1", 52 | "karma": "^1.4.1", 53 | "karma-coverage": "^1.1.1", 54 | "karma-mocha": "^1.3.0", 55 | "karma-phantomjs-launcher": "^1.0.2", 56 | "karma-sinon-chai": "^1.2.4", 57 | "karma-sourcemap-loader": "^0.3.7", 58 | "karma-spec-reporter": "0.0.30", 59 | "karma-webpack": "^2.0.2", 60 | "lolex": "^1.5.2", 61 | "mocha": "^3.2.0", 62 | "nightwatch": "^0.9.12", 63 | "opn": "^4.0.2", 64 | "optimize-css-assets-webpack-plugin": "^1.3.0", 65 | "ora": "^1.1.0", 66 | "phantomjs-prebuilt": "^2.1.14", 67 | "rimraf": "^2.6.0", 68 | "sass-loader": "^6.0.3", 69 | "selenium-server": "^3.0.1", 70 | "semver": "^5.3.0", 71 | "sinon": "^1.17.7", 72 | "sinon-chai": "^2.8.0", 73 | "url-loader": "^0.5.7", 74 | "vue-loader": "^11.1.4", 75 | "vue-style-loader": "^2.0.0", 76 | "vue-template-compiler": "^2.2.1", 77 | "webpack": "^2.2.1", 78 | "webpack-bundle-analyzer": "^2.2.1", 79 | "webpack-dev-middleware": "^1.10.0", 80 | "webpack-hot-middleware": "^2.16.1", 81 | "webpack-merge": "^2.6.1" 82 | }, 83 | "engines": { 84 | "node": ">= 4.0.0", 85 | "npm": ">= 3.0.0" 86 | }, 87 | "browserslist": [ 88 | "> 1%", 89 | "last 2 versions", 90 | "not ie <= 8" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/assets/iconpriority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingRay/mindmap/711f488a0b302260607583e40adb3676b4f2e7a8/src/assets/iconpriority.png -------------------------------------------------------------------------------- /src/assets/iconprogress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingRay/mindmap/711f488a0b302260607583e40adb3676b4f2e7a8/src/assets/iconprogress.png -------------------------------------------------------------------------------- /src/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingRay/mindmap/711f488a0b302260607583e40adb3676b4f2e7a8/src/assets/icons.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingRay/mindmap/711f488a0b302260607583e40adb3676b4f2e7a8/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/mold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingRay/mindmap/711f488a0b302260607583e40adb3676b4f2e7a8/src/assets/mold.png -------------------------------------------------------------------------------- /src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 39 | 40 | 41 | 60 | -------------------------------------------------------------------------------- /src/components/editor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /src/components/footer.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | 10 | 12 | -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 61 | 62 | 66 | -------------------------------------------------------------------------------- /src/components/main_editor/main_editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/main_editor/navigator.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 308 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/attachment.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 132 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/edit_del.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 52 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/edit_menu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/insert_box.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 74 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/label_box.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/move_box.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 45 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/progress_box.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 74 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/sequence_box.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 69 | -------------------------------------------------------------------------------- /src/components/top_menu/edit_menu/undo_redo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /src/components/top_menu/select_menu/expand.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 50 | -------------------------------------------------------------------------------- /src/components/top_menu/select_menu/select_menu.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/components/top_menu/select_menu/selection.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 126 | -------------------------------------------------------------------------------- /src/components/top_menu/view_menu/arrange.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /src/components/top_menu/view_menu/font_operation.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 167 | -------------------------------------------------------------------------------- /src/components/top_menu/view_menu/mold.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 57 | -------------------------------------------------------------------------------- /src/components/top_menu/view_menu/style_operation.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 53 | -------------------------------------------------------------------------------- /src/components/top_menu/view_menu/theme.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 77 | -------------------------------------------------------------------------------- /src/components/top_menu/view_menu/view_menu.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import store from './store' 7 | import 'element-ui/lib/theme-default/index.css' 8 | import ElementUI from 'element-ui' 9 | import $ from 'jquery' 10 | 11 | // 'bower_codemirror': './bower_components/codemirror/lib/codemirror.js', 12 | // 'bower_codemirror_xml.js': './bower_components/codemirror/mode/xml/xml.js', 13 | // 'bower_codemirror_javascript': './bower_components/codemirror/mode/javascript/javascript.js', 14 | // 'bower_codemirror_cssjs': './bower_components/codemirror/mode/css/css.js', 15 | // 'bower_codemirror_htmlmixed': './bower_components/codemirror/mode/htmlmixed/htmlmixed.js', 16 | // 'bower_codemirror_markdown': './bower_components/codemirror/mode/markdown/markdown.js', 17 | // 'bower_codemirror_overlay': './bower_components/codemirror/addon/mode/overlay.js', 18 | // 'bower_codemirror_gfm': './bower_components/codemirror/mode/gfm/gfm.js', 19 | // // 'bower_angular-ui-codemirror':'./bower_components/angular-ui-codemirror/ui-codemirror.js', 20 | // 'bower_marked': './bower_components/marked/lib/marked.js', 21 | // 'bower_kity': './bower_components/kity/dist/kity.js', 22 | // 'bower_hotbox': './bower_components/hotbox/hotbox.js', 23 | // 'bower_jsondiff': './bower_components/json-diff/json-diff.js', 24 | // 'bower_kityminder_core': './bower_components/kityminder-core/dist/kityminder.core.js', 25 | // 'bower_color_picker':'./bower_components/color-picker/dist/color-picker.js', 26 | 27 | require('../bower_components/codemirror/lib/codemirror.js') 28 | require('../bower_components/codemirror/mode/xml/xml.js') 29 | require('../bower_components/codemirror/mode/javascript/javascript.js') 30 | require('../bower_components/codemirror/mode/css/css.js') 31 | require('../bower_components/codemirror/mode/htmlmixed/htmlmixed.js') 32 | require('../bower_components/codemirror/mode/markdown/markdown.js') 33 | require('../bower_components/codemirror/addon/mode/overlay.js') 34 | require('../bower_components/codemirror/mode/gfm/gfm.js') 35 | // require('../bower_components/angular-ui-codemirror/ui-codemirror.js') 36 | require('../bower_components/marked/lib/marked.js') 37 | require('../bower_components/kity/dist/kity.js') 38 | require('../bower_components/hotbox/hotbox.js') 39 | require('../bower_components/json-diff/json-diff.js') 40 | require('../bower_components/kityminder-core/dist/kityminder.core.js') 41 | // require('../bower_components/color-picker/dist/color-picker.js') 42 | require('./script/expose-editor.js') 43 | 44 | 45 | Vue.config.productionTip = true 46 | Vue.use(ElementUI) 47 | 48 | 49 | // console.log(editor); 50 | /* eslint-disable no-new */ 51 | new Vue({el: '#app', router, store, template: '', components: { 52 | App 53 | }}) 54 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | // import Hello from '@/components/Hello' 4 | import editor from '@/components/editor' 5 | 6 | 7 | Vue.use(Router) 8 | 9 | export default new Router({ 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'editor', 14 | component: editor 15 | } 16 | ] 17 | }) 18 | -------------------------------------------------------------------------------- /src/script/editor.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | /** 3 | * 运行时 4 | */ 5 | var runtimes = []; 6 | 7 | function assemble(runtime) { 8 | runtimes.push(runtime); 9 | } 10 | 11 | function KMEditor(selector) { 12 | this.selector = selector; 13 | for (var i = 0; i < runtimes.length; i++) { 14 | if (typeof runtimes[i] == 'function') { 15 | runtimes[i].call(this, this); 16 | } 17 | } 18 | } 19 | 20 | KMEditor.assemble = assemble; 21 | 22 | assemble(require('./runtime/container')); 23 | assemble(require('./runtime/fsm')); 24 | assemble(require('./runtime/minder')); 25 | assemble(require('./runtime/receiver')); 26 | assemble(require('./runtime/hotbox')); 27 | assemble(require('./runtime/input')); 28 | assemble(require('./runtime/clipboard-mimetype')); 29 | assemble(require('./runtime/clipboard')); 30 | assemble(require('./runtime/drag')); 31 | assemble(require('./runtime/node')); 32 | assemble(require('./runtime/history')); 33 | assemble(require('./runtime/jumping')); 34 | assemble(require('./runtime/priority')); 35 | assemble(require('./runtime/progress')); 36 | 37 | 38 | return module.exports = KMEditor; 39 | }); 40 | -------------------------------------------------------------------------------- /src/script/expose-editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 打包暴露 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define('expose-editor', function(require, exports, module) { 10 | return module.exports = kityminder.Editor = require('./editor'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/script/hotbox.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | return module.exports = window.HotBox; 3 | }); -------------------------------------------------------------------------------- /src/script/lang.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | }); -------------------------------------------------------------------------------- /src/script/minder.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | // TODO: 屏蔽报错 3 | return module.exports = window.kityminder.Minder; 4 | }); 5 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard-mimetype.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Desc: 新增一个用于处理系统ctrl+c ctrl+v等方式导入导出节点的MIMETYPE处理,如系统不支持clipboardEvent或者是FF则不初始化改class 3 | * @Editor: Naixor 4 | * @Date: 2015.9.21 5 | */ 6 | define(function(require, exports, module) { 7 | function MimeType() { 8 | /** 9 | * 私有变量 10 | */ 11 | var SPLITOR = '\uFEFF'; 12 | var MIMETYPE = { 13 | 'application/km': '\uFFFF' 14 | }; 15 | var SIGN = { 16 | '\uFEFF': 'SPLITOR', 17 | '\uFFFF': 'application/km' 18 | }; 19 | 20 | /** 21 | * 用于将一段纯文本封装成符合其数据格式的文本 22 | * @method process private 23 | * @param {MIMETYPE} mimetype 数据格式 24 | * @param {String} text 原始文本 25 | * @return {String} 符合该数据格式下的文本 26 | * @example 27 | * var str = "123"; 28 | * str = process('application/km', str); // 返回的内容再经过MimeType判断会读取出其数据格式为application/km 29 | * process('text/plain', str); // 若接受到一个非纯文本信息,则会将其转换为新的数据格式 30 | */ 31 | function process(mimetype, text) { 32 | if (!this.isPureText(text)) { 33 | var _mimetype = this.whichMimeType(text); 34 | if (!_mimetype) { 35 | throw new Error('unknow mimetype!'); 36 | }; 37 | text = this.getPureText(text); 38 | }; 39 | if (mimetype === false) { 40 | return text; 41 | }; 42 | return mimetype + SPLITOR + text; 43 | } 44 | 45 | /** 46 | * 注册数据类型的标识 47 | * @method registMimeTypeProtocol public 48 | * @param {String} type 数据类型 49 | * @param {String} sign 标识 50 | */ 51 | this.registMimeTypeProtocol = function(type, sign) { 52 | if (sign && SIGN[sign]) { 53 | throw new Error('sing has registed!'); 54 | } 55 | if (type && !!MIMETYPE[type]) { 56 | throw new Error('mimetype has registed!'); 57 | }; 58 | SIGN[sign] = type; 59 | MIMETYPE[type] = sign; 60 | } 61 | 62 | /** 63 | * 获取已注册数据类型的协议 64 | * @method getMimeTypeProtocol public 65 | * @param {String} type 数据类型 66 | * @param {String} text|undefiend 文本内容或不传入 67 | * @return {String|Function} 68 | * @example 69 | * text若不传入则直接返回对应数据格式的处理(process)方法 70 | * 若传入文本则直接调用对应的process方法进行处理,此时返回处理后的内容 71 | * var m = new MimeType(); 72 | * var kmprocess = m.getMimeTypeProtocol('application/km'); 73 | * kmprocess("123") === m.getMimeTypeProtocol('application/km', "123"); 74 | * 75 | */ 76 | this.getMimeTypeProtocol = function(type, text) { 77 | var mimetype = MIMETYPE[type] || false; 78 | 79 | if (text === undefined) { 80 | return process.bind(this, mimetype); 81 | }; 82 | 83 | return process(mimetype, text); 84 | } 85 | 86 | this.getSpitor = function() { 87 | return SPLITOR; 88 | } 89 | 90 | this.getMimeType = function(sign) { 91 | if (sign !== undefined) { 92 | return SIGN[sign] || null; 93 | }; 94 | return MIMETYPE; 95 | } 96 | } 97 | 98 | MimeType.prototype.isPureText = function(text) { 99 | return !(~text.indexOf(this.getSpitor())); 100 | } 101 | 102 | MimeType.prototype.getPureText = function(text) { 103 | if (this.isPureText(text)) { 104 | return text; 105 | }; 106 | return text.split(this.getSpitor())[1]; 107 | } 108 | 109 | MimeType.prototype.whichMimeType = function(text) { 110 | if (this.isPureText(text)) { 111 | return null; 112 | }; 113 | return this.getMimeType(text.split(this.getSpitor())[0]); 114 | } 115 | 116 | function MimeTypeRuntime() { 117 | if (this.minder.supportClipboardEvent && !kity.Browser.gecko) { 118 | this.MimeType = new MimeType(); 119 | }; 120 | } 121 | 122 | return module.exports = MimeTypeRuntime; 123 | }); -------------------------------------------------------------------------------- /src/script/runtime/clipboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Desc: 处理editor的clipboard事件,只在支持ClipboardEvent并且不是FF的情况下工作 3 | * @Editor: Naixor 4 | * @Date: 2015.9.21 5 | */ 6 | define(function(require, exports, module) { 7 | 8 | function ClipboardRuntime () { 9 | var minder = this.minder; 10 | var Data = window.kityminder.data; 11 | 12 | if (!minder.supportClipboardEvent || kity.Browser.gecko) { 13 | return; 14 | }; 15 | 16 | var fsm = this.fsm; 17 | var receiver = this.receiver; 18 | var MimeType = this.MimeType; 19 | 20 | var kmencode = MimeType.getMimeTypeProtocol('application/km'), 21 | decode = Data.getRegisterProtocol('json').decode; 22 | var _selectedNodes = []; 23 | 24 | /* 25 | * 增加对多节点赋值粘贴的处理 26 | */ 27 | function encode (nodes) { 28 | var _nodes = []; 29 | for (var i = 0, l = nodes.length; i < l; i++) { 30 | _nodes.push(minder.exportNode(nodes[i])); 31 | } 32 | return kmencode(Data.getRegisterProtocol('json').encode(_nodes)); 33 | } 34 | 35 | var beforeCopy = function (e) { 36 | if (document.activeElement == receiver.element) { 37 | var clipBoardEvent = e; 38 | var state = fsm.state(); 39 | 40 | switch (state) { 41 | case 'input': { 42 | break; 43 | } 44 | case 'normal': { 45 | var nodes = [].concat(minder.getSelectedNodes()); 46 | if (nodes.length) { 47 | // 这里由于被粘贴复制的节点的id信息也都一样,故做此算法 48 | // 这里有个疑问,使用node.getParent()或者node.parent会离奇导致出现非选中节点被渲染成选中节点,因此使用isAncestorOf,而没有使用自行回溯的方式 49 | if (nodes.length > 1) { 50 | var targetLevel; 51 | nodes.sort(function(a, b) { 52 | return a.getLevel() - b.getLevel(); 53 | }); 54 | targetLevel = nodes[0].getLevel(); 55 | if (targetLevel !== nodes[nodes.length-1].getLevel()) { 56 | var plevel, pnode, 57 | idx = 0, l = nodes.length, pidx = l-1; 58 | 59 | pnode = nodes[pidx]; 60 | 61 | while (pnode.getLevel() !== targetLevel) { 62 | idx = 0; 63 | while (idx < l && nodes[idx].getLevel() === targetLevel) { 64 | if (nodes[idx].isAncestorOf(pnode)) { 65 | nodes.splice(pidx, 1); 66 | break; 67 | } 68 | idx++; 69 | } 70 | pidx--; 71 | pnode = nodes[pidx]; 72 | } 73 | }; 74 | }; 75 | var str = encode(nodes); 76 | clipBoardEvent.clipboardData.setData('text/plain', str); 77 | } 78 | e.preventDefault(); 79 | break; 80 | } 81 | } 82 | } 83 | } 84 | 85 | var beforeCut = function (e) { 86 | if (document.activeElement == receiver.element) { 87 | if (minder.getStatus() !== 'normal') { 88 | e.preventDefault(); 89 | return; 90 | }; 91 | 92 | var clipBoardEvent = e; 93 | var state = fsm.state(); 94 | 95 | switch (state) { 96 | case 'input': { 97 | break; 98 | } 99 | case 'normal': { 100 | var nodes = minder.getSelectedNodes(); 101 | if (nodes.length) { 102 | clipBoardEvent.clipboardData.setData('text/plain', encode(nodes)); 103 | minder.execCommand('removenode'); 104 | } 105 | e.preventDefault(); 106 | break; 107 | } 108 | } 109 | }; 110 | } 111 | 112 | var beforePaste = function(e) { 113 | if (document.activeElement == receiver.element) { 114 | if (minder.getStatus() !== 'normal') { 115 | e.preventDefault(); 116 | return; 117 | }; 118 | 119 | var clipBoardEvent = e; 120 | var state = fsm.state(); 121 | var textData = clipBoardEvent.clipboardData.getData('text/plain'); 122 | 123 | switch (state) { 124 | case 'input': { 125 | // input状态下如果格式为application/km则不进行paste操作 126 | if (!MimeType.isPureText(textData)) { 127 | e.preventDefault(); 128 | return; 129 | }; 130 | break; 131 | } 132 | case 'normal': { 133 | /* 134 | * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理 135 | */ 136 | var sNodes = minder.getSelectedNodes(); 137 | 138 | if (MimeType.whichMimeType(textData) === 'application/km') { 139 | var nodes = decode(MimeType.getPureText(textData)); 140 | var _node; 141 | sNodes.forEach(function(node) { 142 | // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来 143 | for (var i = nodes.length-1; i >= 0; i--) { 144 | _node = minder.createNode(null, node); 145 | minder.importNode(_node, nodes[i]); 146 | _selectedNodes.push(_node); 147 | node.appendChild(_node); 148 | } 149 | }); 150 | minder.select(_selectedNodes, true); 151 | _selectedNodes = []; 152 | 153 | minder.refresh(); 154 | } 155 | else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) { 156 | var imageFile = clipBoardEvent.clipboardData.items[0].getAsFile(); 157 | var serverService = angular.element(document.body).injector().get('server'); 158 | 159 | return serverService.uploadImage(imageFile).then(function (json) { 160 | var resp = json.data; 161 | if (resp.errno === 0) { 162 | minder.execCommand('image', resp.data.url); 163 | } 164 | }); 165 | } 166 | else { 167 | sNodes.forEach(function(node) { 168 | minder.Text2Children(node, textData); 169 | }); 170 | } 171 | e.preventDefault(); 172 | break; 173 | } 174 | } 175 | } 176 | } 177 | /** 178 | * 由editor的receiver统一处理全部事件,包括clipboard事件 179 | * @Editor: Naixor 180 | * @Date: 2015.9.24 181 | */ 182 | document.addEventListener('copy', beforeCopy); 183 | document.addEventListener('cut', beforeCut); 184 | document.addEventListener('paste', beforePaste); 185 | } 186 | 187 | return module.exports = ClipboardRuntime; 188 | }); -------------------------------------------------------------------------------- /src/script/runtime/container.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 初始化编辑器的容器 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | 11 | /** 12 | * 最先执行的 Runtime,初始化编辑器容器 13 | */ 14 | function ContainerRuntime() { 15 | var container; 16 | 17 | if (typeof(this.selector) == 'string') { 18 | container = document.querySelector(this.selector); 19 | } else { 20 | container = this.selector; 21 | } 22 | 23 | if (!container) throw new Error('Invalid selector: ' + this.selector); 24 | 25 | // 这个类名用于给编辑器添加样式 26 | container.classList.add('km-editor'); 27 | 28 | // 暴露容器给其他运行时使用 29 | this.container = container; 30 | } 31 | 32 | return module.exports = ContainerRuntime; 33 | }); -------------------------------------------------------------------------------- /src/script/runtime/drag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 用于拖拽节点时屏蔽键盘事件 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | 11 | var Hotbox = require('../hotbox'); 12 | var Debug = require('../tool/debug'); 13 | var debug = new Debug('drag'); 14 | 15 | function DragRuntime() { 16 | var fsm = this.fsm; 17 | var minder = this.minder; 18 | var hotbox = this.hotbox; 19 | var receiver = this.receiver; 20 | var receiverElement = receiver.element; 21 | 22 | // setup everything to go 23 | setupFsm(); 24 | 25 | // listen the fsm changes, make action. 26 | function setupFsm() { 27 | 28 | // when jumped to drag mode, enter 29 | fsm.when('* -> drag', function() { 30 | // now is drag mode 31 | }); 32 | 33 | fsm.when('drag -> *', function(exit, enter, reason) { 34 | if (reason == 'drag-finish') { 35 | // now exit drag mode 36 | } 37 | }); 38 | } 39 | 40 | var downX, downY; 41 | var MOUSE_HAS_DOWN = 0; 42 | var MOUSE_HAS_UP = 1; 43 | var BOUND_CHECK = 20; 44 | var flag = MOUSE_HAS_UP; 45 | var maxX, maxY, osx, osy, containerY; 46 | var freeHorizen = false, freeVirtical = false; 47 | var frame; 48 | 49 | function move(direction, speed) { 50 | if (!direction) { 51 | freeHorizen = freeVirtical = false; 52 | frame && kity.releaseFrame(frame); 53 | frame = null; 54 | return; 55 | } 56 | if (!frame) { 57 | frame = kity.requestFrame((function (direction, speed, minder) { 58 | return function (frame) { 59 | switch (direction) { 60 | case 'left': 61 | minder._viewDragger.move({x: -speed, y: 0}, 0); 62 | break; 63 | case 'top': 64 | minder._viewDragger.move({x: 0, y: -speed}, 0); 65 | break; 66 | case 'right': 67 | minder._viewDragger.move({x: speed, y: 0}, 0); 68 | break; 69 | case 'bottom': 70 | minder._viewDragger.move({x: 0, y: speed}, 0); 71 | break; 72 | default: 73 | return; 74 | } 75 | frame.next(); 76 | }; 77 | })(direction, speed, minder)); 78 | } 79 | } 80 | 81 | minder.on('mousedown', function(e) { 82 | flag = MOUSE_HAS_DOWN; 83 | var rect = minder.getPaper().container.getBoundingClientRect(); 84 | downX = e.originEvent.clientX; 85 | downY = e.originEvent.clientY; 86 | containerY = rect.top; 87 | maxX = rect.width; 88 | maxY = rect.height; 89 | }); 90 | 91 | minder.on('mousemove', function(e) { 92 | if (fsm.state() === 'drag' && flag == MOUSE_HAS_DOWN && minder.getSelectedNode() 93 | && (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK 94 | || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) { 95 | osx = e.originEvent.clientX; 96 | osy = e.originEvent.clientY - containerY; 97 | 98 | if (osx < BOUND_CHECK) { 99 | move('right', BOUND_CHECK - osx); 100 | } else if (osx > maxX - BOUND_CHECK) { 101 | move('left', BOUND_CHECK + osx - maxX); 102 | } else { 103 | freeHorizen = true; 104 | } 105 | if (osy < BOUND_CHECK) { 106 | move('bottom', osy); 107 | } else if (osy > maxY - BOUND_CHECK) { 108 | move('top', BOUND_CHECK + osy - maxY); 109 | } else { 110 | freeVirtical = true; 111 | } 112 | if (freeHorizen && freeVirtical) { 113 | move(false); 114 | } 115 | } 116 | if (fsm.state() !== 'drag' 117 | && flag === MOUSE_HAS_DOWN 118 | && minder.getSelectedNode() 119 | && (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK 120 | || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) { 121 | 122 | if (fsm.state() === 'hotbox') { 123 | hotbox.active(Hotbox.STATE_IDLE); 124 | } 125 | 126 | return fsm.jump('drag', 'user-drag'); 127 | } 128 | }); 129 | 130 | window.addEventListener('mouseup', function () { 131 | flag = MOUSE_HAS_UP; 132 | if (fsm.state() === 'drag') { 133 | move(false); 134 | return fsm.jump('normal', 'drag-finish'); 135 | } 136 | }, false); 137 | } 138 | 139 | return module.exports = DragRuntime; 140 | }); 141 | -------------------------------------------------------------------------------- /src/script/runtime/fsm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 编辑器状态机 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | 11 | var Debug = require('../tool/debug'); 12 | var debug = new Debug('fsm'); 13 | 14 | function handlerConditionMatch(condition, when, exit, enter) { 15 | if (condition.when != when) return false; 16 | if (condition.enter != '*' && condition.enter != enter) return false; 17 | if (condition.exit != '*' && condition.exit != exit) return; 18 | return true; 19 | } 20 | 21 | function FSM(defaultState) { 22 | var currentState = defaultState; 23 | var BEFORE_ARROW = ' - '; 24 | var AFTER_ARROW = ' -> '; 25 | var handlers = []; 26 | 27 | /** 28 | * 状态跳转 29 | * 30 | * 会通知所有的状态跳转监视器 31 | * 32 | * @param {string} newState 新状态名称 33 | * @param {any} reason 跳转的原因,可以作为参数传递给跳转监视器 34 | */ 35 | this.jump = function(newState, reason) { 36 | if (!reason) throw new Error('Please tell fsm the reason to jump'); 37 | 38 | var oldState = currentState; 39 | var notify = [oldState, newState].concat([].slice.call(arguments, 1)); 40 | var i, handler; 41 | 42 | // 跳转前 43 | for (i = 0; i < handlers.length; i++) { 44 | handler = handlers[i]; 45 | if (handlerConditionMatch(handler.condition, 'before', oldState, newState)) { 46 | if (handler.apply(null, notify)) return; 47 | } 48 | } 49 | 50 | currentState = newState; 51 | debug.log('[{0}] {1} -> {2}', reason, oldState, newState); 52 | 53 | // 跳转后 54 | for (i = 0; i < handlers.length; i++) { 55 | handler = handlers[i]; 56 | if (handlerConditionMatch(handler.condition, 'after', oldState, newState)) { 57 | handler.apply(null, notify); 58 | } 59 | } 60 | return currentState; 61 | }; 62 | 63 | /** 64 | * 返回当前状态 65 | * @return {string} 66 | */ 67 | this.state = function() { 68 | return currentState; 69 | }; 70 | 71 | /** 72 | * 添加状态跳转监视器 73 | * 74 | * @param {string} condition 75 | * 监视的时机 76 | * "* => *" (默认) 77 | * 78 | * @param {Function} handler 79 | * 监视函数,当状态跳转的时候,会接收三个参数 80 | * * from - 跳转前的状态 81 | * * to - 跳转后的状态 82 | * * reason - 跳转的原因 83 | */ 84 | this.when = function(condition, handler) { 85 | if (arguments.length == 1) { 86 | handler = condition; 87 | condition = '* -> *'; 88 | } 89 | 90 | var when, resolved, exit, enter; 91 | 92 | resolved = condition.split(BEFORE_ARROW); 93 | if (resolved.length == 2) { 94 | when = 'before'; 95 | } else { 96 | resolved = condition.split(AFTER_ARROW); 97 | if (resolved.length == 2) { 98 | when = 'after'; 99 | } 100 | } 101 | if (!when) throw new Error('Illegal fsm condition: ' + condition); 102 | 103 | exit = resolved[0]; 104 | enter = resolved[1]; 105 | 106 | handler.condition = { 107 | when: when, 108 | exit: exit, 109 | enter: enter 110 | }; 111 | 112 | handlers.push(handler); 113 | }; 114 | } 115 | 116 | function FSMRumtime() { 117 | this.fsm = new FSM('normal'); 118 | } 119 | 120 | return module.exports = FSMRumtime; 121 | }); -------------------------------------------------------------------------------- /src/script/runtime/history.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 历史管理 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | import jsonDiff from '../tool/jsondiff' 10 | // define(function (require, exports, module) { 11 | // var jsonDiff = require('../tool/jsondiff'); 12 | function HistoryRuntime() { 13 | var minder = this.minder; 14 | var hotbox = this.hotbox; 15 | 16 | var MAX_HISTORY = 100; 17 | 18 | var lastSnap; 19 | var patchLock; 20 | var undoDiffs; 21 | var redoDiffs; 22 | 23 | function reset() { 24 | undoDiffs = []; 25 | redoDiffs = []; 26 | lastSnap = minder.exportJson(); 27 | } 28 | 29 | function makeUndoDiff() { 30 | var headSnap = minder.exportJson(); 31 | var diff = jsonDiff(headSnap, lastSnap); 32 | if (diff.length) { 33 | undoDiffs.push(diff); 34 | while (undoDiffs.length > MAX_HISTORY) { 35 | undoDiffs.shift(); 36 | } 37 | lastSnap = headSnap; 38 | return true; 39 | } 40 | } 41 | 42 | function makeRedoDiff() { 43 | var revertSnap = minder.exportJson(); 44 | redoDiffs.push(jsonDiff(revertSnap, lastSnap)); 45 | lastSnap = revertSnap; 46 | } 47 | 48 | function undo() { 49 | patchLock = true; 50 | var undoDiff = undoDiffs.pop(); 51 | if (undoDiff) { 52 | minder.applyPatches(undoDiff); 53 | makeRedoDiff(); 54 | } 55 | patchLock = false; 56 | } 57 | 58 | function redo() { 59 | patchLock = true; 60 | var redoDiff = redoDiffs.pop(); 61 | if (redoDiff) { 62 | minder.applyPatches(redoDiff); 63 | makeUndoDiff(); 64 | } 65 | patchLock = false; 66 | } 67 | 68 | function changed() { 69 | if (patchLock) 70 | return; 71 | if (makeUndoDiff()) 72 | redoDiffs = []; 73 | } 74 | 75 | function hasUndo() { 76 | return !!undoDiffs.length; 77 | } 78 | 79 | function hasRedo() { 80 | return !!redoDiffs.length; 81 | } 82 | 83 | function updateSelection(e) { 84 | if (!patchLock) 85 | return; 86 | var patch = e.patch; 87 | switch (patch.express) { 88 | case 'node.add': 89 | minder.select(patch.node.getChild(patch.index), true); 90 | break; 91 | case 'node.remove': 92 | case 'data.replace': 93 | case 'data.remove': 94 | case 'data.add': 95 | minder.select(patch.node, true); 96 | break; 97 | } 98 | } 99 | 100 | this.history = { 101 | reset: reset, 102 | undo: undo, 103 | redo: redo, 104 | hasUndo: hasUndo, 105 | hasRedo: hasRedo 106 | }; 107 | reset(); 108 | minder.on('contentchange', changed); 109 | minder.on('import', reset); 110 | minder.on('patch', updateSelection); 111 | 112 | var main = hotbox.state('main'); 113 | main.button({ 114 | position: 'top', 115 | label: '撤销', 116 | key: 'Ctrl + Z', 117 | enable: hasUndo, 118 | action: undo, 119 | next: 'idle' 120 | }); 121 | main.button({ 122 | position: 'top', 123 | label: '重做', 124 | key: 'Ctrl + Y', 125 | enable: hasRedo, 126 | action: redo, 127 | next: 'idle' 128 | }); 129 | } 130 | 131 | window.diff = jsonDiff; 132 | 133 | // return module.exports = HistoryRuntime; 134 | // }); 135 | export {HistoryRuntime} 136 | -------------------------------------------------------------------------------- /src/script/runtime/hotbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 热盒 Runtime 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | var Hotbox = require('../hotbox'); 11 | 12 | function HotboxRuntime() { 13 | var fsm = this.fsm; 14 | var minder = this.minder; 15 | var receiver = this.receiver; 16 | var container = this.container; 17 | 18 | var hotbox = new Hotbox(container); 19 | 20 | hotbox.setParentFSM(fsm); 21 | 22 | fsm.when('normal -> hotbox', function(exit, enter, reason) { 23 | var node = minder.getSelectedNode(); 24 | var position; 25 | if (node) { 26 | var box = node.getRenderBox(); 27 | position = { 28 | x: box.cx, 29 | y: box.cy 30 | }; 31 | } 32 | hotbox.active('main', position); 33 | }); 34 | 35 | fsm.when('normal -> normal', function(exit, enter, reason, e) { 36 | if (reason == 'shortcut-handle') { 37 | var handleResult = hotbox.dispatch(e); 38 | if (handleResult) { 39 | e.preventDefault(); 40 | } else { 41 | minder.dispatchKeyEvent(e); 42 | } 43 | } 44 | }); 45 | 46 | fsm.when('modal -> normal', function(exit, enter, reason, e) { 47 | if (reason == 'import-text-finish') { 48 | receiver.element.focus(); 49 | } 50 | }); 51 | 52 | this.hotbox = hotbox; 53 | } 54 | 55 | return module.exports = HotboxRuntime; 56 | }); -------------------------------------------------------------------------------- /src/script/runtime/input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 文本输入支持 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | 11 | require('../tool/innertext'); 12 | 13 | var Debug = require('../tool/debug'); 14 | var debug = new Debug('input'); 15 | 16 | function InputRuntime() { 17 | var fsm = this.fsm; 18 | var minder = this.minder; 19 | var hotbox = this.hotbox; 20 | var receiver = this.receiver; 21 | var receiverElement = receiver.element; 22 | var isGecko = window.kity.Browser.gecko; 23 | 24 | // setup everything to go 25 | setupReciverElement(); 26 | setupFsm(); 27 | setupHotbox(); 28 | 29 | // expose editText() 30 | this.editText = editText; 31 | 32 | 33 | // listen the fsm changes, make action. 34 | function setupFsm() { 35 | 36 | // when jumped to input mode, enter 37 | fsm.when('* -> input', enterInputMode); 38 | 39 | // when exited, commit or exit depends on the exit reason 40 | fsm.when('input -> *', function(exit, enter, reason) { 41 | switch (reason) { 42 | case 'input-cancel': 43 | return exitInputMode(); 44 | case 'input-commit': 45 | default: 46 | return commitInputResult(); 47 | } 48 | }); 49 | 50 | // lost focus to commit 51 | receiver.onblur(function (e) { 52 | if (fsm.state() == 'input') { 53 | fsm.jump('normal', 'input-commit'); 54 | } 55 | }); 56 | 57 | minder.on('beforemousedown', function () { 58 | if (fsm.state() == 'input') { 59 | fsm.jump('normal', 'input-commit'); 60 | } 61 | }); 62 | 63 | minder.on('dblclick', function() { 64 | if (minder.getSelectedNode() && minder._status !== 'readonly') { 65 | editText(); 66 | } 67 | }); 68 | } 69 | 70 | 71 | // let the receiver follow the current selected node position 72 | function setupReciverElement() { 73 | if (debug.flaged) { 74 | receiverElement.classList.add('debug'); 75 | } 76 | 77 | receiverElement.onmousedown = function(e) { 78 | e.stopPropagation(); 79 | }; 80 | 81 | minder.on('layoutallfinish viewchange viewchanged selectionchange', function(e) { 82 | 83 | // viewchange event is too frequenced, lazy it 84 | if (e.type == 'viewchange' && fsm.state() != 'input') return; 85 | 86 | updatePosition(); 87 | }); 88 | 89 | updatePosition(); 90 | } 91 | 92 | 93 | // edit entrance in hotbox 94 | function setupHotbox() { 95 | hotbox.state('main').button({ 96 | position: 'center', 97 | label: '编辑', 98 | key: 'F2', 99 | enable: function() { 100 | return minder.queryCommandState('text') != -1; 101 | }, 102 | action: editText 103 | }); 104 | } 105 | 106 | 107 | /** 108 | * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致 109 | * @editor Naixor 110 | * @Date 2015-12-2 111 | */ 112 | // edit for the selected node 113 | function editText() { 114 | var node = minder.getSelectedNode(); 115 | if (!node) { 116 | return; 117 | } 118 | var textContainer = receiverElement; 119 | receiverElement.innerText = ""; 120 | if (node.getData('font-weight') === 'bold') { 121 | var b = document.createElement('b'); 122 | textContainer.appendChild(b); 123 | textContainer = b; 124 | } 125 | if (node.getData('font-style') === 'italic') { 126 | var i = document.createElement('i'); 127 | textContainer.appendChild(i); 128 | textContainer = i; 129 | } 130 | textContainer.innerText = minder.queryCommandValue('text'); 131 | 132 | if (isGecko) { 133 | receiver.fixFFCaretDisappeared(); 134 | }; 135 | fsm.jump('input', 'input-request'); 136 | receiver.selectAll(); 137 | } 138 | 139 | /** 140 | * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致 141 | * @editor Naixor 142 | * @Date 2015-12-2 143 | */ 144 | function enterInputMode() { 145 | var node = minder.getSelectedNode(); 146 | if (node) { 147 | var fontSize = node.getData('font-size') || node.getStyle('font-size'); 148 | receiverElement.style.fontSize = fontSize + 'px'; 149 | receiverElement.style.minWidth = 0; 150 | receiverElement.style.minWidth = receiverElement.clientWidth + 'px'; 151 | receiverElement.style.fontWeight = node.getData('font-weight') || ''; 152 | receiverElement.style.fontStyle = node.getData('font-style') || ''; 153 | receiverElement.classList.add('input'); 154 | receiverElement.focus(); 155 | } 156 | } 157 | 158 | /** 159 | * 按照文本提交操作处理 160 | * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样试用一下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生 161 | * @Warning: 下方代码使用[].slice.call来将HTMLDomCollection处理成为Array,ie8及以下会有问题 162 | * @Editor: Naixor 163 | * @Date: 2015.9.16 164 | */ 165 | function commitInputText (textNodes) { 166 | var text = ''; 167 | var TAB_CHAR = '\t', 168 | ENTER_CHAR = '\n', 169 | STR_CHECK = /\S/, 170 | SPACE_CHAR = '\u0020', 171 | // 针对FF,SG,BD,LB,IE等浏览器下SPACE的charCode存在为32和160的情况做处理 172 | SPACE_CHAR_REGEXP = new RegExp('(\u0020|' + String.fromCharCode(160) + ')'), 173 | BR = document.createElement('br'); 174 | var isBold = false, 175 | isItalic = false; 176 | 177 | for (var str, 178 | _divChildNodes, 179 | space_l, space_num, tab_num, 180 | i = 0, l = textNodes.length; i < l; i++) { 181 | str = textNodes[i]; 182 | 183 | switch (Object.prototype.toString.call(str)) { 184 | // 正常情况处理 185 | case '[object HTMLBRElement]': { 186 | text += ENTER_CHAR; 187 | break; 188 | } 189 | case '[object Text]': { 190 | // SG下会莫名其妙的加上 影响后续判断,干掉! 191 | /** 192 | * FF下的wholeText会导致如下问题: 193 | * |123| -> 在一个节点中输入一段字符,此时TextNode为[#Text 123] 194 | * 提交并重新编辑,在后面追加几个字符 195 | * |123abc| -> 此时123为一个TextNode为[#Text 123, #Text abc],但是对这两个任意取值wholeText均为全部内容123abc 196 | * 上述BUG仅存在在FF中,故将wholeText更改为textContent 197 | */ 198 | str = str.textContent.replace(" ", " "); 199 | 200 | if (!STR_CHECK.test(str)) { 201 | space_l = str.length; 202 | while (space_l--) { 203 | if (SPACE_CHAR_REGEXP.test(str[space_l])) { 204 | text += SPACE_CHAR; 205 | } else if (str[space_l] === TAB_CHAR) { 206 | text += TAB_CHAR; 207 | } 208 | } 209 | } else { 210 | text += str; 211 | } 212 | break; 213 | } 214 | // ctrl + b/i 会给字体加上/标签来实现黑体和斜体 215 | case '[object HTMLElement]': { 216 | switch (str.nodeName) { 217 | case "B": { 218 | isBold = true; 219 | break; 220 | } 221 | case "I": { 222 | isItalic = true; 223 | break; 224 | } 225 | default: {} 226 | } 227 | [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes))); 228 | l = textNodes.length; 229 | i--; 230 | break; 231 | } 232 | // 被增加span标签的情况会被处理成正常情况并会推交给上面处理 233 | case '[object HTMLSpanElement]': { 234 | [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes))); 235 | l = textNodes.length; 236 | i--; 237 | break; 238 | } 239 | // 若标签为image标签,则判断是否为合法url,是将其加载进来 240 | case '[object HTMLImageElement]': { 241 | if (str.src) { 242 | if (/http(|s):\/\//.test(str.src)) { 243 | minder.execCommand("Image", str.src, str.alt); 244 | } else { 245 | // data:image协议情况 246 | } 247 | }; 248 | break; 249 | } 250 | // 被增加div标签的情况会被处理成正常情况并会推交给上面处理 251 | case '[object HTMLDivElement]': { 252 | _divChildNodes = []; 253 | for (var di = 0, l = str.childNodes.length; di < l; di++) { 254 | _divChildNodes.push(str.childNodes[di]); 255 | } 256 | _divChildNodes.push(BR); 257 | [].splice.apply(textNodes, [i, 1].concat(_divChildNodes)); 258 | l = textNodes.length; 259 | i--; 260 | break; 261 | } 262 | default: { 263 | if (str && str.childNodes.length) { 264 | _divChildNodes = []; 265 | for (var di = 0, l = str.childNodes.length; di < l; di++) { 266 | _divChildNodes.push(str.childNodes[di]); 267 | } 268 | _divChildNodes.push(BR); 269 | [].splice.apply(textNodes, [i, 1].concat(_divChildNodes)); 270 | l = textNodes.length; 271 | i--; 272 | } else { 273 | if (str && str.textContent !== undefined) { 274 | text += str.textContent; 275 | } else { 276 | text += ""; 277 | } 278 | } 279 | // // 其他带有样式的节点被粘贴进来,则直接取textContent,若取不出来则置空 280 | } 281 | } 282 | }; 283 | 284 | text = text.replace(/^\n*|\n*$/g, ''); 285 | text = text.replace(new RegExp('(\n|\r|\n\r)(\u0020|' + String.fromCharCode(160) + '){4}', 'g'), '$1\t'); 286 | minder.getSelectedNode().setText(text); 287 | if (isBold) { 288 | minder.queryCommandState('bold') || minder.execCommand('bold'); 289 | } else { 290 | minder.queryCommandState('bold') && minder.execCommand('bold'); 291 | } 292 | 293 | if (isItalic) { 294 | minder.queryCommandState('italic') || minder.execCommand('italic'); 295 | } else { 296 | minder.queryCommandState('italic') && minder.execCommand('italic'); 297 | } 298 | exitInputMode(); 299 | return text; 300 | } 301 | 302 | /** 303 | * 判断节点的文本信息是否是 304 | * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样使用以下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生 305 | * @Notice: 此处逻辑应该拆分到 kityminder-core/core/data中去,单独增加一个对某个节点importJson的事件 306 | * @Editor: Naixor 307 | * @Date: 2015.9.16 308 | */ 309 | function commitInputNode(node, text) { 310 | try { 311 | minder.decodeData('text', text).then(function(json) { 312 | function importText(node, json, minder) { 313 | var data = json.data; 314 | 315 | node.setText(data.text || ''); 316 | 317 | var childrenTreeData = json.children || []; 318 | for (var i = 0; i < childrenTreeData.length; i++) { 319 | var childNode = minder.createNode(null, node); 320 | importText(childNode, childrenTreeData[i], minder); 321 | } 322 | return node; 323 | } 324 | importText(node, json, minder); 325 | minder.fire("contentchange"); 326 | minder.getRoot().renderTree(); 327 | minder.layout(300); 328 | }); 329 | } catch (e) { 330 | minder.fire("contentchange"); 331 | minder.getRoot().renderTree(); 332 | 333 | // 无法被转换成脑图节点则不处理 334 | if (e.toString() !== 'Error: Invalid local format') { 335 | throw e; 336 | } 337 | } 338 | } 339 | 340 | function commitInputResult() { 341 | /** 342 | * @Desc: 进行如下处理: 343 | * 根据用户的输入判断是否生成新的节点 344 | * fix #83 https://github.com/fex-team/kityminder-editor/issues/83 345 | * @Editor: Naixor 346 | * @Date: 2015.9.16 347 | */ 348 | var textNodes = [].slice.call(receiverElement.childNodes); 349 | 350 | /** 351 | * @Desc: 增加setTimeout的原因:ie下receiverElement.innerHTML=""会导致后 352 | * 面commitInputText中使用textContent报错,不要问我什么原因! 353 | * @Editor: Naixor 354 | * @Date: 2015.12.14 355 | */ 356 | setTimeout(function () { 357 | // 解决过大内容导致SVG窜位问题 358 | receiverElement.innerHTML = ""; 359 | }, 0); 360 | var node = minder.getSelectedNode(); 361 | 362 | textNodes = commitInputText(textNodes); 363 | commitInputNode(node, textNodes); 364 | 365 | if (node.type == 'root') { 366 | var rootText = minder.getRoot().getText(); 367 | minder.fire('initChangeRoot', {text: rootText}); 368 | } 369 | } 370 | 371 | function exitInputMode() { 372 | receiverElement.classList.remove('input'); 373 | receiver.selectAll(); 374 | } 375 | 376 | function updatePosition() { 377 | var planed = updatePosition; 378 | 379 | var focusNode = minder.getSelectedNode(); 380 | if (!focusNode) return; 381 | 382 | if (!planed.timer) { 383 | planed.timer = setTimeout(function() { 384 | var box = focusNode.getRenderBox('TextRenderer'); 385 | receiverElement.style.left = Math.round(box.x) + 'px'; 386 | receiverElement.style.top = (debug.flaged ? Math.round(box.bottom + 30) : Math.round(box.y)) + 'px'; 387 | //receiverElement.focus(); 388 | planed.timer = 0; 389 | }); 390 | } 391 | } 392 | } 393 | 394 | return module.exports = InputRuntime; 395 | }); 396 | -------------------------------------------------------------------------------- /src/script/runtime/jumping.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 根据按键控制状态机的跳转 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | 11 | var Hotbox = require('../hotbox'); 12 | 13 | 14 | // Nice: http://unixpapa.com/js/key.html 15 | function isIntendToInput(e) { 16 | if (e.ctrlKey || e.metaKey || e.altKey) return false; 17 | 18 | // a-zA-Z 19 | if (e.keyCode >= 65 && e.keyCode <= 90) return true; 20 | 21 | // 0-9 以及其上面的符号 22 | if (e.keyCode >= 48 && e.keyCode <= 57) return true; 23 | 24 | // 小键盘区域 (除回车外) 25 | if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 26 | 27 | // 小键盘区域 (除回车外) 28 | // @yinheli from pull request 29 | if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 30 | 31 | // 输入法 32 | if (e.keyCode == 229 || e.keyCode === 0) return true; 33 | 34 | return false; 35 | } 36 | /** 37 | * @Desc: 下方使用receiver.enable()和receiver.disable()通过 38 | * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug; 39 | * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户 40 | * 输入法状态丢失,因此对FF暂不做处理 41 | * @Editor: Naixor 42 | * @Date: 2015.09.14 43 | */ 44 | function JumpingRuntime() { 45 | var fsm = this.fsm; 46 | var minder = this.minder; 47 | var receiver = this.receiver; 48 | var container = this.container; 49 | var receiverElement = receiver.element; 50 | var hotbox = this.hotbox; 51 | 52 | // normal -> * 53 | receiver.listen('normal', function(e) { 54 | // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable 55 | receiver.enable(); 56 | // normal -> hotbox 57 | if (e.is('Space')) { 58 | e.preventDefault(); 59 | // safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉 60 | if (kity.Browser.safari) { 61 | receiverElement.innerHTML = ''; 62 | } 63 | return fsm.jump('hotbox', 'space-trigger'); 64 | } 65 | 66 | /** 67 | * check 68 | * @editor Naixor 69 | * @Date 2015-12-2 70 | */ 71 | switch (e.type) { 72 | case 'keydown': { 73 | if (minder.getSelectedNode()) { 74 | if (isIntendToInput(e)) { 75 | return fsm.jump('input', 'user-input'); 76 | }; 77 | } else { 78 | receiverElement.innerHTML = ''; 79 | } 80 | // normal -> normal shortcut 81 | fsm.jump('normal', 'shortcut-handle', e); 82 | break; 83 | } 84 | case 'keyup': { 85 | break; 86 | } 87 | default: {} 88 | } 89 | }); 90 | 91 | // hotbox -> normal 92 | receiver.listen('hotbox', function(e) { 93 | receiver.disable(); 94 | e.preventDefault(); 95 | var handleResult = hotbox.dispatch(e); 96 | if (hotbox.state() == Hotbox.STATE_IDLE && fsm.state() == 'hotbox') { 97 | return fsm.jump('normal', 'hotbox-idle'); 98 | } 99 | }); 100 | 101 | // input => normal 102 | receiver.listen('input', function(e) { 103 | receiver.enable(); 104 | if (e.type == 'keydown') { 105 | if (e.is('Enter')) { 106 | e.preventDefault(); 107 | return fsm.jump('normal', 'input-commit'); 108 | } 109 | if (e.is('Esc')) { 110 | e.preventDefault(); 111 | return fsm.jump('normal', 'input-cancel'); 112 | } 113 | if (e.is('Tab') || e.is('Shift + Tab')) { 114 | e.preventDefault(); 115 | } 116 | } else if (e.type == 'keyup' && e.is('Esc')) { 117 | e.preventDefault(); 118 | return fsm.jump('normal', 'input-cancel'); 119 | } 120 | }); 121 | 122 | 123 | ////////////////////////////////////////////// 124 | /// 右键呼出热盒 125 | /// 判断的标准是:按下的位置和结束的位置一致 126 | ////////////////////////////////////////////// 127 | var downX, downY; 128 | var MOUSE_RB = 2; // 右键 129 | 130 | container.addEventListener('mousedown', function(e) { 131 | if (e.button == MOUSE_RB) { 132 | e.preventDefault(); 133 | } 134 | if (fsm.state() == 'hotbox') { 135 | hotbox.active(Hotbox.STATE_IDLE); 136 | fsm.jump('normal', 'blur'); 137 | } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) { 138 | downX = e.clientX; 139 | downY = e.clientY; 140 | } 141 | }, false); 142 | 143 | container.addEventListener('mousewheel', function(e) { 144 | if (fsm.state() == 'hotbox') { 145 | hotbox.active(Hotbox.STATE_IDLE); 146 | fsm.jump('normal', 'mousemove-blur'); 147 | } 148 | }, false); 149 | 150 | container.addEventListener('contextmenu', function(e) { 151 | e.preventDefault(); 152 | }); 153 | 154 | container.addEventListener('mouseup', function(e) { 155 | if (fsm.state() != 'normal') { 156 | return; 157 | } 158 | if (e.button != MOUSE_RB || e.clientX != downX || e.clientY != downY) { 159 | return; 160 | } 161 | if (!minder.getSelectedNode()) { 162 | return; 163 | } 164 | fsm.jump('hotbox', 'content-menu'); 165 | }, false); 166 | 167 | // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭 168 | hotbox.$element.addEventListener('mousedown', function(e) { 169 | e.stopPropagation(); 170 | }); 171 | } 172 | 173 | return module.exports = JumpingRuntime; 174 | }); 175 | -------------------------------------------------------------------------------- /src/script/runtime/minder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 脑图示例运行时 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | var Minder = require('../minder'); 11 | function MinderRuntime() { 12 | 13 | // 不使用 kityminder 的按键处理,由 ReceiverRuntime 统一处理 14 | var minder = new Minder({ 15 | enableKeyReceiver: false, 16 | enableAnimation: true 17 | }); 18 | 19 | // 渲染,初始化 20 | minder.renderTo(this.selector); 21 | minder.setTheme(null); 22 | minder.select(minder.getRoot(), true); 23 | minder.execCommand('text', '中心主题'); 24 | 25 | // 导出给其它 Runtime 使用 26 | this.minder = minder; 27 | } 28 | 29 | return module.exports = MinderRuntime; 30 | }); 31 | -------------------------------------------------------------------------------- /src/script/runtime/node.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | 3 | function NodeRuntime() { 4 | var runtime = this; 5 | var minder = this.minder; 6 | var hotbox = this.hotbox; 7 | var fsm = this.fsm; 8 | 9 | var main = hotbox.state('main'); 10 | 11 | var buttons = [ 12 | '前移:Alt+Up:ArrangeUp', 13 | '下级:Tab|Insert:AppendChildNode', 14 | '同级:Enter:AppendSiblingNode', 15 | '后移:Alt+Down:ArrangeDown', 16 | '删除:Delete|Backspace:RemoveNode', 17 | '上级:Shift+Tab|Shift+Insert:AppendParentNode' 18 | //'全选:Ctrl+A:SelectAll' 19 | ]; 20 | 21 | var AppendLock = 0; 22 | 23 | buttons.forEach(function(button) { 24 | var parts = button.split(':'); 25 | var label = parts.shift(); 26 | var key = parts.shift(); 27 | var command = parts.shift(); 28 | main.button({ 29 | position: 'ring', 30 | label: label, 31 | key: key, 32 | action: function() { 33 | if (command.indexOf('Append') === 0) { 34 | AppendLock++; 35 | minder.execCommand(command, '分支主题'); 36 | 37 | // provide in input runtime 38 | function afterAppend () { 39 | if (!--AppendLock) { 40 | runtime.editText(); 41 | } 42 | minder.off('layoutallfinish', afterAppend); 43 | } 44 | minder.on('layoutallfinish', afterAppend); 45 | } else { 46 | minder.execCommand(command); 47 | fsm.jump('normal', 'command-executed'); 48 | } 49 | }, 50 | enable: function() { 51 | return minder.queryCommandState(command) != -1; 52 | } 53 | }); 54 | }); 55 | 56 | main.button({ 57 | position: 'bottom', 58 | label: '导入节点', 59 | key: 'Alt + V', 60 | enable: function() { 61 | var selectedNodes = minder.getSelectedNodes(); 62 | return selectedNodes.length == 1; 63 | }, 64 | action: importNodeData, 65 | next: 'idle' 66 | }); 67 | 68 | main.button({ 69 | position: 'bottom', 70 | label: '导出节点', 71 | key: 'Alt + C', 72 | enable: function() { 73 | var selectedNodes = minder.getSelectedNodes(); 74 | return selectedNodes.length == 1; 75 | }, 76 | action: exportNodeData, 77 | next: 'idle' 78 | }); 79 | 80 | function importNodeData() { 81 | minder.fire('importNodeData'); 82 | } 83 | 84 | function exportNodeData() { 85 | minder.fire('exportNodeData'); 86 | } 87 | 88 | //main.button({ 89 | // position: 'ring', 90 | // key: '/', 91 | // action: function(){ 92 | // if (!minder.queryCommandState('expand')) { 93 | // minder.execCommand('expand'); 94 | // } else if (!minder.queryCommandState('collapse')) { 95 | // minder.execCommand('collapse'); 96 | // } 97 | // }, 98 | // enable: function() { 99 | // return minder.queryCommandState('expand') != -1 || minder.queryCommandState('collapse') != -1; 100 | // }, 101 | // beforeShow: function() { 102 | // if (!minder.queryCommandState('expand')) { 103 | // this.$button.children[0].innerHTML = '展开'; 104 | // } else { 105 | // this.$button.children[0].innerHTML = '收起'; 106 | // } 107 | // } 108 | //}) 109 | } 110 | 111 | return module.exports = NodeRuntime; 112 | }); 113 | -------------------------------------------------------------------------------- /src/script/runtime/priority.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module){ 2 | 3 | function PriorityRuntime() { 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | 7 | var main = hotbox.state('main'); 8 | 9 | main.button({ 10 | position: 'top', 11 | label: '优先级', 12 | key: 'P', 13 | next: 'priority', 14 | enable: function() { 15 | return minder.queryCommandState('priority') != -1; 16 | } 17 | }); 18 | 19 | var priority = hotbox.state('priority'); 20 | '123456789'.replace(/./g, function(p) { 21 | priority.button({ 22 | position: 'ring', 23 | label: 'P' + p, 24 | key: p, 25 | action: function() { 26 | minder.execCommand('Priority', p); 27 | } 28 | }); 29 | }); 30 | 31 | priority.button({ 32 | position: 'center', 33 | label: '移除', 34 | key: 'Del', 35 | action: function() { 36 | minder.execCommand('Priority', 0); 37 | } 38 | }); 39 | 40 | priority.button({ 41 | position: 'top', 42 | label: '返回', 43 | key: 'esc', 44 | next: 'back' 45 | }); 46 | 47 | } 48 | 49 | return module.exports = PriorityRuntime; 50 | 51 | }); -------------------------------------------------------------------------------- /src/script/runtime/progress.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module){ 2 | 3 | function ProgressRuntime() { 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | 7 | var main = hotbox.state('main'); 8 | 9 | main.button({ 10 | position: 'top', 11 | label: '进度', 12 | key: 'G', 13 | next: 'progress', 14 | enable: function() { 15 | return minder.queryCommandState('progress') != -1; 16 | } 17 | }); 18 | 19 | var progress = hotbox.state('progress'); 20 | '012345678'.replace(/./g, function(p) { 21 | progress.button({ 22 | position: 'ring', 23 | label: 'G' + p, 24 | key: p, 25 | action: function() { 26 | minder.execCommand('Progress', parseInt(p) + 1); 27 | } 28 | }); 29 | }); 30 | 31 | progress.button({ 32 | position: 'center', 33 | label: '移除', 34 | key: 'Del', 35 | action: function() { 36 | minder.execCommand('Progress', 0); 37 | } 38 | }); 39 | 40 | progress.button({ 41 | position: 'top', 42 | label: '返回', 43 | key: 'esc', 44 | next: 'back' 45 | }); 46 | 47 | 48 | } 49 | 50 | return module.exports = ProgressRuntime; 51 | 52 | }); -------------------------------------------------------------------------------- /src/script/runtime/receiver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 键盘事件接收/分发器 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | define(function(require, exports, module) { 10 | var key = require('../tool/key'); 11 | var hotbox = require('./hotbox'); 12 | 13 | function ReceiverRuntime() { 14 | var fsm = this.fsm; 15 | var minder = this.minder; 16 | var me = this; 17 | 18 | // 接收事件的 div 19 | var element = document.createElement('div'); 20 | element.contentEditable = true; 21 | /** 22 | * @Desc: 增加tabindex属性使得element的contenteditable不管是trur还是false都能有focus和blur事件 23 | * @Editor: Naixor 24 | * @Date: 2015.09.14 25 | */ 26 | element.setAttribute("tabindex", -1); 27 | element.classList.add('receiver'); 28 | element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent; 29 | this.container.appendChild(element); 30 | 31 | // receiver 对象 32 | var receiver = { 33 | element: element, 34 | selectAll: function() { 35 | // 保证有被选中的 36 | if (!element.innerHTML) element.innerHTML = ' '; 37 | var range = document.createRange(); 38 | var selection = window.getSelection(); 39 | range.selectNodeContents(element); 40 | selection.removeAllRanges(); 41 | selection.addRange(range); 42 | element.focus(); 43 | }, 44 | /** 45 | * @Desc: 增加enable和disable方法用于解决热核态的输入法屏蔽问题 46 | * @Editor: Naixor 47 | * @Date: 2015.09.14 48 | */ 49 | enable: function() { 50 | element.setAttribute("contenteditable", true); 51 | }, 52 | disable: function() { 53 | element.setAttribute("contenteditable", false); 54 | }, 55 | /** 56 | * @Desc: hack FF下div contenteditable的光标丢失BUG 57 | * @Editor: Naixor 58 | * @Date: 2015.10.15 59 | */ 60 | fixFFCaretDisappeared: function() { 61 | element.removeAttribute("contenteditable"); 62 | element.setAttribute("contenteditable", "true"); 63 | element.blur(); 64 | element.focus(); 65 | }, 66 | /** 67 | * 以此事件代替通过mouse事件来判断receiver丢失焦点的事件 68 | * @editor Naixor 69 | * @Date 2015-12-2 70 | */ 71 | onblur: function (handler) { 72 | element.onblur = handler; 73 | } 74 | }; 75 | receiver.selectAll(); 76 | minder.on('beforemousedown', receiver.selectAll); 77 | minder.on('receiverfocus', receiver.selectAll); 78 | minder.on('readonly', function() { 79 | // 屏蔽minder的事件接受,删除receiver和hotbox 80 | minder.disable(); 81 | editor.receiver.element.parentElement.removeChild(editor.receiver.element); 82 | editor.hotbox.$container.removeChild(editor.hotbox.$element); 83 | }); 84 | 85 | // 侦听器,接收到的事件会派发给所有侦听器 86 | var listeners = []; 87 | 88 | // 侦听指定状态下的事件,如果不传 state,侦听所有状态 89 | receiver.listen = function(state, listener) { 90 | if (arguments.length == 1) { 91 | listener = state; 92 | state = '*'; 93 | } 94 | listener.notifyState = state; 95 | listeners.push(listener); 96 | }; 97 | 98 | function dispatchKeyEvent(e) { 99 | e.is = function(keyExpression) { 100 | var subs = keyExpression.split('|'); 101 | for (var i = 0; i < subs.length; i++) { 102 | if (key.is(this, subs[i])) return true; 103 | } 104 | return false; 105 | }; 106 | var listener, jumpState; 107 | for (var i = 0; i < listeners.length; i++) { 108 | 109 | listener = listeners[i]; 110 | // 忽略不在侦听状态的侦听器 111 | if (listener.notifyState != '*' && listener.notifyState != fsm.state()) { 112 | continue; 113 | } 114 | 115 | /** 116 | * 117 | * 对于所有的侦听器,只允许一种处理方式:跳转状态。 118 | * 如果侦听器确定要跳转,则返回要跳转的状态。 119 | * 每个事件只允许一个侦听器进行状态跳转 120 | * 跳转动作由侦听器自行完成(因为可能需要在跳转时传递 reason),返回跳转结果即可。 121 | * 比如: 122 | * 123 | * ```js 124 | * receiver.listen('normal', function(e) { 125 | * if (isSomeReasonForJumpState(e)) { 126 | * return fsm.jump('newstate', e); 127 | * } 128 | * }); 129 | * ``` 130 | */ 131 | if (listener.call(null, e)) { 132 | return; 133 | } 134 | } 135 | } 136 | 137 | this.receiver = receiver; 138 | } 139 | 140 | return module.exports = ReceiverRuntime; 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /src/script/tool/debug.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @fileOverview 4 | * 5 | * 支持各种调试后门 6 | * 7 | * @author: techird 8 | * @copyright: Baidu FEX, 2014 9 | */ 10 | define(function(require, exports, module) { 11 | var format = require('./format'); 12 | 13 | function noop() {} 14 | 15 | function stringHash(str) { 16 | var hash = 0; 17 | for (var i = 0; i < str.length; i++) { 18 | hash += str.charCodeAt(i); 19 | } 20 | return hash; 21 | } 22 | 23 | /* global console */ 24 | function Debug(flag) { 25 | var debugMode = this.flaged = window.location.search.indexOf(flag) != -1; 26 | 27 | if (debugMode) { 28 | var h = stringHash(flag) % 360; 29 | 30 | var flagStyle = format( 31 | 'background: hsl({0}, 50%, 80%); ' + 32 | 'color: hsl({0}, 100%, 30%); ' + 33 | 'padding: 2px 3px; ' + 34 | 'margin: 1px 3px 0 0;' + 35 | 'border-radius: 2px;', h); 36 | 37 | var textStyle = 'background: none; color: black;'; 38 | this.log = function() { 39 | var output = format.apply(null, arguments); 40 | console.log(format('%c{0}%c{1}', flag, output), flagStyle, textStyle); 41 | }; 42 | } else { 43 | this.log = noop; 44 | } 45 | } 46 | 47 | return module.exports = Debug; 48 | }); 49 | -------------------------------------------------------------------------------- /src/script/tool/format.js: -------------------------------------------------------------------------------- 1 | // define(function(require, exports, module) { 2 | // function format(template, args) { 3 | // if (typeof(args) != 'object') { 4 | // args = [].slice.call(arguments, 1); 5 | // } 6 | // return String(template).replace(/\{(\w+)\}/ig, function(match, $key) { 7 | // return args[$key] || $key; 8 | // }); 9 | // } 10 | // return module.exports = format; 11 | // }); 12 | 13 | function format(template, args) { 14 | if (typeof(args) != 'object') { 15 | args = [].slice.call(arguments, 1); 16 | } 17 | return String(template).replace(/\{(\w+)\}/ig, function (match, $key) { 18 | return args[$key] || $key; 19 | }); 20 | } 21 | export {format} 22 | -------------------------------------------------------------------------------- /src/script/tool/innertext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * innerText polyfill 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | 10 | 11 | define(function(require, exports, module) { 12 | if ((!('innerText' in document.createElement('a'))) && ('getSelection' in window)) { 13 | HTMLElement.prototype.__defineGetter__('innerText', function() { 14 | var selection = window.getSelection(), 15 | ranges = [], 16 | str, i; 17 | 18 | // Save existing selections. 19 | for (i = 0; i < selection.rangeCount; i++) { 20 | ranges[i] = selection.getRangeAt(i); 21 | } 22 | 23 | // Deselect everything. 24 | selection.removeAllRanges(); 25 | 26 | // Select `el` and all child nodes. 27 | // 'this' is the element .innerText got called on 28 | selection.selectAllChildren(this); 29 | 30 | // Get the string representation of the selected nodes. 31 | str = selection.toString(); 32 | 33 | // Deselect everything. Again. 34 | selection.removeAllRanges(); 35 | 36 | // Restore all formerly existing selections. 37 | for (i = 0; i < ranges.length; i++) { 38 | selection.addRange(ranges[i]); 39 | } 40 | 41 | // Oh look, this is what we wanted. 42 | // String representation of the element, close to as rendered. 43 | return str; 44 | }); 45 | HTMLElement.prototype.__defineSetter__('innerText', function(text) { 46 | /** 47 | * @Desc: 解决FireFox节点内容删除后text为null,出现报错的问题 48 | * @Editor: Naixor 49 | * @Date: 2015.9.16 50 | */ 51 | this.innerHTML = (text || '').replace(//g, '>').replace(/\n/g, '
'); 52 | }); 53 | } 54 | }); -------------------------------------------------------------------------------- /src/script/tool/jsondiff.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview 3 | * 4 | * 5 | * 6 | * @author: techird 7 | * @copyright: Baidu FEX, 2014 8 | */ 9 | 10 | // define(function (require, exports, module) { 11 | /*! 12 | * https://github.com/Starcounter-Jack/Fast-JSON-Patch 13 | * json-patch-duplex.js 0.5.0 14 | * (c) 2013 Joachim Wester 15 | * MIT license 16 | */ 17 | 18 | var _objectKeys = (function () { 19 | if (Object.keys) 20 | return Object.keys; 21 | 22 | return function (o) { 23 | var keys = []; 24 | for (var i in o) { 25 | if (o.hasOwnProperty(i)) { 26 | keys.push(i); 27 | } 28 | } 29 | return keys; 30 | }; 31 | })(); 32 | function escapePathComponent(str) { 33 | if (str.indexOf('/') === -1 && str.indexOf('~') === -1) 34 | return str; 35 | return str.replace(/~/g, '~0').replace(/\//g, '~1'); 36 | } 37 | function deepClone(obj) { 38 | if (typeof obj === "object") { 39 | return JSON.parse(JSON.stringify(obj)); 40 | } else { 41 | return obj; 42 | } 43 | } 44 | 45 | // Dirty check if obj is different from mirror, generate patches and update mirror 46 | function _generate(mirror, obj, patches, path) { 47 | var newKeys = _objectKeys(obj); 48 | var oldKeys = _objectKeys(mirror); 49 | var changed = false; 50 | var deleted = false; 51 | 52 | for (var t = oldKeys.length - 1; t >= 0; t--) { 53 | var key = oldKeys[t]; 54 | var oldVal = mirror[key]; 55 | if (obj.hasOwnProperty(key)) { 56 | var newVal = obj[key]; 57 | if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { 58 | _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); 59 | } else { 60 | if (oldVal != newVal) { 61 | changed = true; 62 | patches.push({ 63 | op: "replace", 64 | path: path + "/" + escapePathComponent(key), 65 | value: deepClone(newVal) 66 | }); 67 | } 68 | } 69 | } else { 70 | patches.push({ 71 | op: "remove", 72 | path: path + "/" + escapePathComponent(key) 73 | }); 74 | deleted = true; // property has been deleted 75 | } 76 | } 77 | 78 | if (!deleted && newKeys.length == oldKeys.length) { 79 | return; 80 | } 81 | 82 | for (var t = 0; t < newKeys.length; t++) { 83 | var key = newKeys[t]; 84 | if (!mirror.hasOwnProperty(key)) { 85 | patches.push({ 86 | op: "add", 87 | path: path + "/" + escapePathComponent(key), 88 | value: deepClone(obj[key]) 89 | }); 90 | } 91 | } 92 | } 93 | 94 | export default function compare(tree1, tree2) { 95 | var patches = []; 96 | _generate(tree1, tree2, patches, ''); 97 | return patches; 98 | } 99 | 100 | // return module.exports = compare; 101 | // }); 102 | // 103 | -------------------------------------------------------------------------------- /src/script/tool/key.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | var keymap = require('./keymap'); 3 | 4 | var CTRL_MASK = 0x1000; 5 | var ALT_MASK = 0x2000; 6 | var SHIFT_MASK = 0x4000; 7 | 8 | function hash(unknown) { 9 | if (typeof(unknown) == 'string') { 10 | return hashKeyExpression(unknown); 11 | } 12 | return hashKeyEvent(unknown); 13 | } 14 | function is(a, b) { 15 | return a && b && hash(a) == hash(b); 16 | } 17 | exports.hash = hash; 18 | exports.is = is; 19 | 20 | 21 | function hashKeyEvent(keyEvent) { 22 | var hashCode = 0; 23 | if (keyEvent.ctrlKey || keyEvent.metaKey) { 24 | hashCode |= CTRL_MASK; 25 | } 26 | if (keyEvent.altKey) { 27 | hashCode |= ALT_MASK; 28 | } 29 | if (keyEvent.shiftKey) { 30 | hashCode |= SHIFT_MASK; 31 | } 32 | // Shift, Control, Alt KeyCode ignored. 33 | if ([16, 17, 18, 91].indexOf(keyEvent.keyCode) === -1) { 34 | /** 35 | * 解决浏览器输入法状态下对keyDown的keyCode判断不准确的问题,使用keyIdentifier, 36 | * 可以解决chrome和safari下的各种问题,其他浏览器依旧有问题,然而那并不影响我们对特 37 | * 需判断的按键进行判断(比如Space在safari输入法态下就是229,其他的就不是) 38 | * @editor Naixor 39 | * @Date 2015-12-2 40 | */ 41 | if (keyEvent.keyCode === 229 && keyEvent.keyIdentifier) { 42 | return hashCode |= parseInt(keyEvent.keyIdentifier.substr(2), 16); 43 | } 44 | hashCode |= keyEvent.keyCode; 45 | } 46 | return hashCode; 47 | } 48 | 49 | function hashKeyExpression(keyExpression) { 50 | var hashCode = 0; 51 | keyExpression.toLowerCase().split(/\s*\+\s*/).forEach(function(name) { 52 | switch(name) { 53 | case 'ctrl': 54 | case 'cmd': 55 | hashCode |= CTRL_MASK; 56 | break; 57 | case 'alt': 58 | hashCode |= ALT_MASK; 59 | break; 60 | case 'shift': 61 | hashCode |= SHIFT_MASK; 62 | break; 63 | default: 64 | hashCode |= keymap[name]; 65 | } 66 | }); 67 | return hashCode; 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /src/script/tool/keymap.js: -------------------------------------------------------------------------------- 1 | define(function(require, exports, module) { 2 | var keymap = { 3 | 4 | 'Shift': 16, 5 | 'Control': 17, 6 | 'Alt': 18, 7 | 'CapsLock': 20, 8 | 9 | 'BackSpace': 8, 10 | 'Tab': 9, 11 | 'Enter': 13, 12 | 'Esc': 27, 13 | 'Space': 32, 14 | 15 | 'PageUp': 33, 16 | 'PageDown': 34, 17 | 'End': 35, 18 | 'Home': 36, 19 | 20 | 'Insert': 45, 21 | 22 | 'Left': 37, 23 | 'Up': 38, 24 | 'Right': 39, 25 | 'Down': 40, 26 | 27 | 'Direction': { 28 | 37: 1, 29 | 38: 1, 30 | 39: 1, 31 | 40: 1 32 | }, 33 | 34 | 'Del': 46, 35 | 36 | 'NumLock': 144, 37 | 38 | 'Cmd': 91, 39 | 'CmdFF': 224, 40 | 'F1': 112, 41 | 'F2': 113, 42 | 'F3': 114, 43 | 'F4': 115, 44 | 'F5': 116, 45 | 'F6': 117, 46 | 'F7': 118, 47 | 'F8': 119, 48 | 'F9': 120, 49 | 'F10': 121, 50 | 'F11': 122, 51 | 'F12': 123, 52 | 53 | '`': 192, 54 | '=': 187, 55 | '-': 189, 56 | 57 | '/': 191, 58 | '.': 190 59 | }; 60 | 61 | // 小写适配 62 | for (var key in keymap) { 63 | if (keymap.hasOwnProperty(key)) { 64 | keymap[key.toLowerCase()] = keymap[key]; 65 | } 66 | } 67 | var aKeyCode = 65; 68 | var aCharCode = 'a'.charCodeAt(0); 69 | 70 | // letters 71 | 'abcdefghijklmnopqrstuvwxyz'.split('').forEach(function(letter) { 72 | keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode); 73 | }); 74 | 75 | // numbers 76 | var n = 9; 77 | do { 78 | keymap[n.toString()] = n + 48; 79 | } while (--n); 80 | 81 | module.exports = keymap; 82 | }); -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | export const actions = { 2 | changeCount: ({commit}) => commit('changeCount'), 3 | 4 | increment: ({commit}) => commit('increment'), 5 | 6 | decrement: ({commit}) => commit('decrement'), 7 | 8 | incrementIfOdd({commit, state}) { 9 | if ((state.count + 1) % 2 === 0) { 10 | commit('increment') 11 | } 12 | }, 13 | 14 | incrementAsync({commit}) { 15 | return new Promise((resolve, reject) => { 16 | setTimeout(() => { 17 | commit('increment') 18 | resolve() 19 | }, 1000) 20 | }) 21 | }, 22 | 23 | setConfig: ({commit}) => commit('setConfig'), 24 | 25 | registerEvent: ({commit}) => commit('registerEvent', callback), 26 | 27 | executeCallback({commit, state}) { 28 | // console.log(state.callbackQueue); 29 | state.callbackQueue.forEach(function (ele) { 30 | ele.apply(this, arguments); 31 | }) 32 | }, 33 | 34 | //localStorage 35 | isQuotaExceeded(e) { 36 | var quotaExceeded = false; 37 | if (e) { 38 | if (e.code) { 39 | switch (e.code) { 40 | case 22: 41 | quotaExceeded = true; 42 | break; 43 | case 1014: 44 | // Firefox 45 | if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { 46 | quotaExceeded = true; 47 | } 48 | break; 49 | } 50 | } else if (e.number === -2147024882) { 51 | // Internet Explorer 8 52 | quotaExceeded = true; 53 | } 54 | } 55 | return quotaExceeded;index 56 | }, 57 | 58 | getMemory({commit,state},key) { 59 | var value = window.localStorage.getItem(key); 60 | var result = null || JSON.parse(value) 61 | console.log('action:'+result); 62 | return result; 63 | }, 64 | 65 | setMemory({commit,state}, data) { 66 | try { 67 | // console.log(data); 68 | window.localStorage.setItem(data.key, JSON.stringify(data.value)); 69 | return true; 70 | } catch (e) { 71 | if (this.isQuotaExceeded(e)) { 72 | return false; 73 | } 74 | } 75 | }, 76 | 77 | removeMemory(key) { 78 | var value = window.localStorage.getItem(key); 79 | window.localStorage.removeItem(key); 80 | return value; 81 | }, 82 | 83 | clearMemory() { 84 | window.localStorage.clear(); 85 | }, 86 | 87 | // commandBinder(minder, command, scope) { 88 | // minder.on && minder.on('interactchange', function () { 89 | // console.log(1); 90 | // scope.commandDisabled = minder.queryCommandState(command) === -1; 91 | // scope.commandValue = minder.queryCommandValue(command); 92 | // }); 93 | // } 94 | } 95 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | // getters are functions 2 | export const count = state => { 3 | // console.log(state.state.count); 4 | return state.count; 5 | } 6 | 7 | export const working = state => { 8 | return { 9 | saving: state.working.saving, 10 | draging: state.working.draging, 11 | editing: state.working.editing 12 | } 13 | } 14 | 15 | export const config = state => { 16 | return { 17 | ctrlPanelMin: state.config.ctrlPanelMin, 18 | ctrlPanelWidth: state.config.ctrlPanelWidth, 19 | dividerWidth: state.config.dividerWidth, 20 | defaultLang: state.config.defaultLang, 21 | zoom: state.config.zoom 22 | } 23 | } 24 | 25 | export const getMinder = state=>{ 26 | return state.minder 27 | } 28 | 29 | export const getEditor = state=>{ 30 | return state.editor 31 | } 32 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import * as getters from './getters' 5 | import {actions} from './actions' 6 | import {mutations} from './mutations' 7 | import {state} from './state' 8 | 9 | Vue.use(Vuex) 10 | // console.log('mu'); 11 | // console.log( mutations); 12 | // console.log('ac'); 13 | // console.log(actions); 14 | 15 | const store = new Vuex.Store({state, getters, actions, mutations}) 16 | 17 | // console.log(store); 18 | export default store 19 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export const mutations = { 2 | 3 | changeDrag(state, bool) { 4 | state.working.draging = bool; 5 | }, 6 | 7 | setMinder(state, data) { 8 | state.minder = data 9 | }, 10 | 11 | setEditor(state, data) { 12 | state.editor = data 13 | }, 14 | 15 | changeSave(state, bool) { 16 | state.working.saving = bool; 17 | }, 18 | 19 | changeCount(state) { 20 | state.count++; 21 | }, 22 | 23 | increment(state) { 24 | state.count++ 25 | }, 26 | 27 | decrement(state) { 28 | state.count-- 29 | }, 30 | 31 | registerEvent(state, callback) { 32 | state.callbackQueue.push(callback); 33 | }, 34 | 35 | setConfig(state) { 36 | var supported = Object.keys(state.config); 37 | var configObj = {}; 38 | 39 | // 支持全配置 40 | if (typeof key === 'object') { 41 | configObj = key; 42 | } else { 43 | configObj[key] = value; 44 | } 45 | 46 | for (var i in configObj) { 47 | if (configObj.hasOwnProperty(i) && supported.indexOf(i) !== -1) { 48 | state.config[i] = configObj[i]; 49 | } else { 50 | console.error('Unsupported config key: ', key, ', please choose in :', supported.join(', ')); 51 | return false; 52 | } 53 | } 54 | return true; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | export const state = { 2 | count: 2, 3 | minder:{}, 4 | editor:{}, 5 | working: { 6 | editing: false, 7 | saving: false, 8 | draging: false 9 | }, 10 | callbackQueue: [], 11 | config: { 12 | // 右侧面板最小宽度 13 | ctrlPanelMin: 250, 14 | 15 | // 右侧面板宽度 16 | ctrlPanelWidth: parseInt(window.localStorage.__dev_minder_ctrlPanelWidth) || 250, 17 | 18 | // 分割线宽度 19 | dividerWidth: 3, 20 | 21 | // 默认语言 22 | defaultLang: 'zh-cn', 23 | 24 | // 放大缩小比例 25 | zoom: [ 26 | 10, 27 | 20, 28 | 30, 29 | 50, 30 | 80, 31 | 100, 32 | 120, 33 | 150, 34 | 200 35 | ], 36 | 37 | // 图片上传接口 38 | imageUpload: 'server/imageUpload.php' 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/style/dropdown-list.scss: -------------------------------------------------------------------------------- 1 | .link-dropdown-list, 2 | .img-dropdown-list, 3 | .remark-dropdown-list, 4 | .selection-dropdown-list, 5 | .expand-dropdown-list { 6 | font-size: 12px 7 | } 8 | 9 | .mold-dropdown-list { 10 | width: 126px; 11 | height: 170px; 12 | font-size: 12px; 13 | .mold-icons { 14 | background-image: url('../assets/mold.png'); 15 | background-repeat: no-repeat; 16 | } 17 | .dropdown-item { 18 | display: inline-block; 19 | width: 50px; 20 | height: 40px; 21 | padding: 0; 22 | margin: 5px; 23 | } 24 | @for $i from 1 through 6 { 25 | .mold-#{$i} { 26 | background-position: (1-$i)*50px 0; 27 | } 28 | } 29 | } 30 | 31 | .theme-dropdown-list { 32 | width: 100px; 33 | height: 200px; 34 | font-size: 12px; 35 | .mold-icons { 36 | /* background-image: url('../assets/theme.png'); */ 37 | background-repeat: no-repeat; 38 | } 39 | .dropdown-item { 40 | display: inline-block; 41 | width: 50px; 42 | height: 40px; 43 | padding: 0; 44 | margin: 5px; 45 | } 46 | } 47 | 48 | .expand-dropdown-list { 49 | .dropdown-item { 50 | line-height: 25px; 51 | } 52 | } 53 | 54 | .selection-dropdown-list { 55 | .dropdown-item { 56 | line-height: 25px; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/style/editor.scss: -------------------------------------------------------------------------------- 1 | @import "../../../bower_components/codemirror/lib/codemirror.css"; 2 | @import "../../../bower_components/hotbox/hotbox.css"; 3 | @import "../../../bower_components/kityminder-core/dist/kityminder.core.css"; 4 | @import "../../../bower_components/color-picker/dist/color-picker.css"; 5 | @import "navigator.scss"; 6 | 7 | *{ 8 | box-sizing: border-box; 9 | } 10 | .mind-editor { 11 | position: absolute; 12 | top: 91px; 13 | left: 0; 14 | right: 0; 15 | bottom: 0; 16 | } 17 | 18 | .km-editor { 19 | overflow: hidden; 20 | z-index: 2; 21 | } 22 | 23 | .km-editor > .mask { 24 | display: block; 25 | position: absolute; 26 | left: 0; 27 | right: 0; 28 | top: 0; 29 | bottom: 0; 30 | background-color: transparent; 31 | } 32 | 33 | .km-editor > .receiver { 34 | position: absolute; 35 | background: white; 36 | outline: none; 37 | box-shadow: 0 0 20px fadeout(black, 50%); 38 | left: 0; 39 | top: 0; 40 | padding: 3px 5px; 41 | margin-left: -3px; 42 | margin-top: -5px; 43 | max-width: 300px; 44 | width: auto; 45 | overflow: hidden; 46 | font-size: 14px; 47 | line-height: 1.4em; 48 | min-height: 1.4em; 49 | box-sizing: border-box; 50 | overflow: hidden; 51 | word-break: break-all; 52 | word-wrap: break-word; 53 | border: none; 54 | -webkit-user-select: text; 55 | pointer-events: none; 56 | opacity: 0; 57 | z-index: -1000; 58 | 59 | &.debug { 60 | opacity: 1; 61 | outline: 1px solid green; 62 | background: none; 63 | z-index: 0; 64 | } 65 | 66 | &.input { 67 | pointer-events: all; 68 | opacity: 1; 69 | z-index: 999; 70 | background: white; 71 | outline: none; 72 | } 73 | } 74 | 75 | div.minder-editor-container { 76 | position: absolute; 77 | top: 40px; 78 | bottom: 0; 79 | left: 0; 80 | right: 0; 81 | font-family: Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 82 | } 83 | 84 | .minder-editor { 85 | position: absolute; 86 | top: 92px; 87 | left: 0; 88 | right: 0; 89 | bottom: 0; 90 | } 91 | 92 | .minder-viewer { 93 | position: absolute; 94 | top: 0; 95 | left: 0; 96 | right: 0; 97 | bottom: 0; 98 | } 99 | 100 | .control-panel { 101 | position: absolute; 102 | top: 0; 103 | right: 0; 104 | width: 250px; 105 | bottom: 0; 106 | border-left: 1px solid #CCC; 107 | } 108 | 109 | .minder-divider { 110 | position: absolute; 111 | top: 0; 112 | right: 250px; 113 | bottom: 0; 114 | width: 2px; 115 | background-color: rgb(251, 251, 251); 116 | cursor: ew-resize; 117 | } 118 | -------------------------------------------------------------------------------- /src/style/header.scss: -------------------------------------------------------------------------------- 1 | @import 'mixin.scss'; 2 | @import 'dropdown-list.scss'; 3 | header { 4 | font-size: 12px; 5 | &>ul { 6 | display: flex; 7 | align-items: center; 8 | height: 30px; 9 | margin: 0; 10 | padding: 0; 11 | background-color: #e1e1e1; 12 | li { 13 | line-height: 30px; 14 | display: inline-flex; 15 | width: 50px; 16 | height: 100%; 17 | list-style: none; 18 | a { 19 | font-size: 14px; 20 | width: inherit; 21 | text-align: center; 22 | text-decoration: none; 23 | color: #337ab7; 24 | } 25 | a:hover, 26 | a:focus { 27 | color: #23527c; 28 | } 29 | } 30 | li.selected { 31 | background: #fff; 32 | a { 33 | color: #000; 34 | } 35 | } 36 | } 37 | } 38 | 39 | #mind_tab-content { 40 | height: 61px; 41 | border-bottom: 1px solid #999; 42 | .tab-icons { 43 | background-image: url('../assets/icons.png'); 44 | background-repeat: no-repeat; 45 | } 46 | } 47 | 48 | .mind-tab-panel { 49 | width: 100%; 50 | height: 100%; 51 | .menu-container { 52 | display: flex; 53 | height: inherit; 54 | &>div { 55 | display: inline-flex; 56 | overflow: hidden; 57 | align-items: center; 58 | flex-wrap: wrap; 59 | height: 100%; 60 | border-right: 1px dashed #eee; 61 | } 62 | &>div:last-of-type { 63 | border-right: none; 64 | } 65 | } 66 | .menu-btn { 67 | display: inline-flex; 68 | cursor: pointer; 69 | @include flexcenter; 70 | } 71 | .menu-btn:not([disabled]):hover { 72 | background-color: $btn-hover-color; 73 | } 74 | i { 75 | display: inline-block; 76 | width: 20px; 77 | height: 20px; 78 | } 79 | .do-group { 80 | width: 40px; 81 | height: 100%; 82 | padding: 0 5px; 83 | p { 84 | height: 50%; 85 | margin: 0; 86 | @include flexcenter; 87 | } 88 | .undo i { 89 | background-position: 0 -1240px; 90 | } 91 | .redo i { 92 | background-position: 0 -1220px; 93 | } 94 | } 95 | .insert-group { 96 | width: 230px; 97 | &>div { 98 | height: 50%; 99 | margin: 0 5px; 100 | } 101 | .insert-sibling-box { 102 | i { 103 | background-position: 0 -20px; 104 | } 105 | } 106 | .insert-parent-box { 107 | i { 108 | background-position: 0 -40px; 109 | } 110 | } 111 | } 112 | .edit-del-group, 113 | .move-group { 114 | width: 70px; 115 | @include flexcenter; 116 | } 117 | .move-group { 118 | .move-up { 119 | i { 120 | background-position: 0 -280px; 121 | } 122 | } 123 | .move-down { 124 | i { 125 | background-position: 0 -300px; 126 | } 127 | } 128 | } 129 | .edit-del-group { 130 | .edit { 131 | i { 132 | background-position: 0 -60px; 133 | } 134 | } 135 | .del { 136 | i { 137 | background-position: 0 -80px; 138 | } 139 | } 140 | } 141 | .attachment-group { 142 | width: 185px; 143 | @include flexcenter; 144 | .el-dropdown-link { 145 | font-size: 12px; 146 | } 147 | button { 148 | font-size: inherit; 149 | width: 45px; 150 | height: 20px; 151 | padding: 0; 152 | background-repeat: no-repeat; 153 | background-position: right; 154 | @include button; 155 | @include flexcenter; 156 | span { 157 | margin-left: 15px; 158 | } 159 | } 160 | button:hover { 161 | background-color: $btn-hover-color; 162 | } 163 | &>div { 164 | font-size: inherit; 165 | flex-wrap: wrap; 166 | width: 60px; 167 | height: 100%; 168 | @include flexcenter; 169 | } 170 | .insert { 171 | height: 25px; 172 | background-repeat: no-repeat; 173 | } 174 | .link { 175 | .insert { 176 | background-position: 50% -100px; 177 | } 178 | } 179 | .img { 180 | .insert { 181 | background-position: 50% -125px; 182 | } 183 | } 184 | .remark { 185 | .insert { 186 | background-position: 50% -1150px; 187 | } 188 | } 189 | .el-dropdown{ 190 | cursor: default; 191 | } 192 | } 193 | .progress-group, 194 | .sequence-group { 195 | width: 135px; 196 | @include flexcenter; 197 | ul { 198 | width: 120px; 199 | margin: 0; 200 | padding: 0; 201 | list-style: none; 202 | li { 203 | display: inline-block; 204 | width: 20px; 205 | height: 20px; 206 | margin: 2px; 207 | } 208 | } 209 | } 210 | .sequence-group { 211 | li { 212 | background-image: url('../assets/iconpriority.png'); 213 | } 214 | @for $i from 0 through 9 { 215 | .sequence-#{$i} { 216 | background-position: 0 -20px * (-1+$i); 217 | } 218 | } 219 | } 220 | .progress-group { 221 | li { 222 | background-image: url('../assets/iconprogress.png'); 223 | } 224 | @for $i from 0 through 9 { 225 | .progress-#{$i} { 226 | background-position: 0 -20px * (-1+$i); 227 | } 228 | } 229 | } 230 | .label-group { 231 | width: 200px; 232 | margin: 0 5px; 233 | i { 234 | width: 30px; 235 | height: 30px; 236 | } 237 | .el-input__inner { 238 | height: 30px; 239 | } 240 | .dropdown { 241 | box-sizing: border-box; 242 | width: 100%; 243 | height: 30px; 244 | border: 1px solid #eee; 245 | border-radius: 2px; 246 | } 247 | } 248 | .mold-group { 249 | width: 80px; 250 | @include flexcenter; 251 | .mold-icons { 252 | background-image: url('../assets/mold.png'); 253 | background-repeat: no-repeat; 254 | } 255 | @for $i from 1 through 6 { 256 | .mold-#{$i} { 257 | background-position: (1-$i)*50px 0; 258 | } 259 | } 260 | .dropdown-toggle { 261 | display: block; 262 | width: 50px; 263 | height: 50px; 264 | margin: 5px 0 0 auto; 265 | span { 266 | display: inline-block; 267 | @include block; 268 | i { 269 | position: absolute; 270 | right: -20px; 271 | bottom: -5px; 272 | } 273 | } 274 | } 275 | } 276 | .theme-group { 277 | font-size: 12px; 278 | width: 100px; 279 | a { 280 | display: inline-block; 281 | text-align: center; 282 | text-decoration: none; 283 | @include block; 284 | } 285 | .dropdown-toggle { 286 | width: 80px; 287 | height: 50px; 288 | margin: 6px auto; 289 | span { 290 | font-size: 12px; 291 | display: inline-block; 292 | @include block; 293 | } 294 | &>a { 295 | display: block; 296 | height: 20px; 297 | margin-top: 15px; 298 | } 299 | } 300 | } 301 | .arrange-group { 302 | width: 65px; 303 | .arrange { 304 | flex-wrap: wrap; 305 | @include flexcenter; 306 | } 307 | .tab-icons { 308 | display: inline-block; 309 | width: 25px; 310 | height: 25px; 311 | margin: 0; 312 | background-repeat: no-repeat; 313 | background-position: 0 -150px; 314 | } 315 | } 316 | .style-group { 317 | width: 150px; 318 | .clear-style-btn { 319 | flex-wrap: wrap; 320 | width: 65px; 321 | @include flexcenter; 322 | .tab-icons { 323 | display: inline-block; 324 | width: 25px; 325 | height: 25px; 326 | margin: 0; 327 | background-repeat: no-repeat; 328 | background-position: 0 -175px; 329 | } 330 | } 331 | .copy-paste-panel { 332 | width: 70px; 333 | .tab-icons { 334 | display: inline-block; 335 | width: 20px; 336 | height: 20px; 337 | } 338 | .copy-style { 339 | .tab-icons { 340 | background-position: 0 -200px; 341 | } 342 | } 343 | .paste-style { 344 | .tab-icons { 345 | background-position: 0 -220px; 346 | } 347 | } 348 | } 349 | } 350 | .font-group { 351 | width: 250px; 352 | * { 353 | font-size: 12px; 354 | } 355 | input { 356 | height: 25px; 357 | } 358 | .font-family-select { 359 | input { 360 | width: 150px; 361 | } 362 | } 363 | .font-size-select input { 364 | width: 80px; 365 | } 366 | .font-bold, 367 | .font-italic { 368 | display: inline-block; 369 | width: 20px; 370 | height: 20px; 371 | margin: 0 3px; 372 | } 373 | .font-bold { 374 | background-position: 0 -240px; 375 | } 376 | .font-italic { 377 | background-position: 0 -260px; 378 | } 379 | } 380 | .expand-group, 381 | .selection-group { 382 | width: 60px; 383 | button { 384 | border: none; 385 | outline: none; 386 | } 387 | @include flexcenter; 388 | margin: 0 5px; 389 | span{ 390 | font-size: 12px; 391 | } 392 | } 393 | .expand-group { 394 | .expand { 395 | width: 40px; 396 | height: 25px; 397 | background-position: center -995px; 398 | } 399 | i { 400 | font-size: 12px; 401 | } 402 | } 403 | .selection-group { 404 | .selection { 405 | width: 40px; 406 | height: 25px; 407 | background-position: 7px -1175px; 408 | } 409 | i { 410 | font-size: 12px; 411 | } 412 | } //////////////////////////////////// 413 | } 414 | -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | $btn-hover-color: #eee; 2 | body { 3 | user-select: none; 4 | min-width: 1280px; 5 | height: 360px; 6 | } 7 | 8 | *[disabled] { 9 | opacity: 0.5; 10 | } 11 | 12 | @mixin block { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | @mixin button { 18 | background: transparent; 19 | border: none; 20 | outline: none; 21 | } 22 | 23 | @mixin flexcenter { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | -------------------------------------------------------------------------------- /src/style/navigator.scss: -------------------------------------------------------------------------------- 1 | .nav-bar { 2 | position: absolute; 3 | width: 35px; 4 | height: 240px; 5 | padding: 5px 0; 6 | left: 10px; 7 | bottom: 10px; 8 | background: #fc8383; 9 | color: #fff; 10 | border-radius: 4px; 11 | z-index: 10; 12 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2); 13 | transition: -webkit-transform 0.7s 0.1s ease; 14 | transition: transform 0.7s 0.1s ease; 15 | 16 | .nav-btn { 17 | width: 35px; 18 | height: 24px; 19 | line-height: 24px; 20 | text-align: center; 21 | 22 | .icon { 23 | background: url("../../assets/icons.png"); 24 | width: 20px; 25 | height: 20px; 26 | margin: 2px auto; 27 | display: block; 28 | } 29 | 30 | &.active { 31 | background-color: #5A6378; 32 | } 33 | } 34 | 35 | .zoom-in .icon { 36 | background-position: 0 -730px; 37 | } 38 | 39 | .zoom-out .icon { 40 | background-position: 0 -750px; 41 | } 42 | 43 | .hand .icon { 44 | background-position: 0 -770px; 45 | width: 25px; 46 | height: 25px; 47 | margin: 0 auto; 48 | } 49 | 50 | .camera .icon { 51 | background-position: 0 -870px; 52 | width: 25px; 53 | height: 25px; 54 | margin: 0 auto; 55 | } 56 | 57 | .nav-trigger .icon { 58 | background-position: 0 -845px; 59 | width: 25px; 60 | height: 25px; 61 | margin: 0 auto; 62 | } 63 | 64 | .zoom-pan { 65 | width: 2px; 66 | height: 70px; 67 | box-shadow: 0 1px #E50000; 68 | position: relative; 69 | background: white; 70 | margin: 3px auto; 71 | overflow: visible; 72 | 73 | .origin { 74 | position: absolute; 75 | width: 20px; 76 | height: 8px; 77 | left: -9px; 78 | margin-top: -4px; 79 | background: transparent; 80 | 81 | &:after { 82 | content: ' '; 83 | display: block; 84 | width: 6px; 85 | height: 2px; 86 | background: white; 87 | left: 7px; 88 | top: 3px; 89 | position: absolute; 90 | } 91 | } 92 | 93 | .indicator { 94 | position: absolute; 95 | width: 8px; 96 | height: 8px; 97 | left: -3px; 98 | background: white; 99 | border-radius: 100%; 100 | margin-top: -4px; 101 | } 102 | 103 | } 104 | } 105 | 106 | .nav-previewer { 107 | background: #fff; 108 | width: 140px; 109 | height: 120px; 110 | position: absolute; 111 | left: 45px; 112 | bottom: 30px; 113 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 114 | border-radius: 0 2px 2px 0; 115 | padding: 1px; 116 | z-index: 9; 117 | cursor: crosshair; 118 | transition: -webkit-transform 0.7s 0.1s ease; 119 | transition: transform 0.7s 0.1s ease; 120 | 121 | &.grab { 122 | cursor: move; 123 | cursor: -webkit-grabbing; 124 | cursor: -moz-grabbing; 125 | cursor: grabbing; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/style/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | 4 | /* Document 5 | ========================================================================== */ 6 | 7 | 8 | /** 9 | * 1. Change the default font family in all browsers (opinionated). 10 | * 2. Correct the line height in all browsers. 11 | * 3. Prevent adjustments of font size after orientation changes in 12 | * IE on Windows Phone and in iOS. 13 | */ 14 | 15 | html { 16 | font-family: sans-serif; 17 | /* 1 */ 18 | line-height: 1.15; 19 | /* 2 */ 20 | -ms-text-size-adjust: 100%; 21 | /* 3 */ 22 | -webkit-text-size-adjust: 100%; 23 | /* 3 */ 24 | } 25 | 26 | 27 | /* Sections 28 | ========================================================================== */ 29 | 30 | 31 | /** 32 | * Remove the margin in all browsers (opinionated). 33 | */ 34 | 35 | body { 36 | margin: 0; 37 | } 38 | 39 | 40 | /** 41 | * Add the correct display in IE 9-. 42 | */ 43 | 44 | article, 45 | aside, 46 | footer, 47 | header, 48 | nav, 49 | section { 50 | display: block; 51 | } 52 | 53 | 54 | /** 55 | * Correct the font size and margin on `h1` elements within `section` and 56 | * `article` contexts in Chrome, Firefox, and Safari. 57 | */ 58 | 59 | h1 { 60 | font-size: 2em; 61 | margin: .67em 0; 62 | } 63 | 64 | 65 | /* Grouping content 66 | ========================================================================== */ 67 | 68 | 69 | /** 70 | * Add the correct display in IE 9-. 71 | * 1. Add the correct display in IE. 72 | */ 73 | 74 | figcaption, 75 | figure, 76 | main { 77 | /* 1 */ 78 | display: block; 79 | } 80 | 81 | 82 | /** 83 | * Add the correct margin in IE 8. 84 | */ 85 | 86 | figure { 87 | margin: 1em 40px; 88 | } 89 | 90 | 91 | /** 92 | * 1. Add the correct box sizing in Firefox. 93 | * 2. Show the overflow in Edge and IE. 94 | */ 95 | 96 | hr { 97 | overflow: visible; 98 | /* 2 */ 99 | box-sizing: content-box; 100 | /* 1 */ 101 | height: 0; 102 | /* 1 */ 103 | } 104 | 105 | 106 | /** 107 | * 1. Correct the inheritance and scaling of font size in all browsers. 108 | * 2. Correct the odd `em` font sizing in all browsers. 109 | */ 110 | 111 | pre { 112 | font-family: monospace, monospace; 113 | /* 1 */ 114 | font-size: 1em; 115 | /* 2 */ 116 | } 117 | 118 | 119 | /* Text-level semantics 120 | ========================================================================== */ 121 | 122 | 123 | /** 124 | * 1. Remove the gray background on active links in IE 10. 125 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 126 | */ 127 | 128 | a { 129 | background-color: transparent; 130 | /* 1 */ 131 | -webkit-text-decoration-skip: objects; 132 | /* 2 */ 133 | } 134 | 135 | 136 | /** 137 | * Remove the outline on focused links when they are also active or hovered 138 | * in all browsers (opinionated). 139 | */ 140 | 141 | a:active, 142 | a:hover { 143 | outline-width: 0; 144 | } 145 | 146 | 147 | /** 148 | * 1. Remove the bottom border in Firefox 39-. 149 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 150 | */ 151 | 152 | abbr[title] { 153 | text-decoration: underline; 154 | /* 2 */ 155 | text-decoration: underline dotted; 156 | /* 2 */ 157 | border-bottom: none; 158 | /* 1 */ 159 | } 160 | 161 | 162 | /** 163 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 164 | */ 165 | 166 | b, 167 | strong { 168 | font-weight: inherit; 169 | } 170 | 171 | 172 | /** 173 | * Add the correct font weight in Chrome, Edge, and Safari. 174 | */ 175 | 176 | b, 177 | strong { 178 | font-weight: bolder; 179 | } 180 | 181 | 182 | /** 183 | * 1. Correct the inheritance and scaling of font size in all browsers. 184 | * 2. Correct the odd `em` font sizing in all browsers. 185 | */ 186 | 187 | code, 188 | kbd, 189 | samp { 190 | font-family: monospace, monospace; 191 | /* 1 */ 192 | font-size: 1em; 193 | /* 2 */ 194 | } 195 | 196 | 197 | /** 198 | * Add the correct font style in Android 4.3-. 199 | */ 200 | 201 | dfn { 202 | font-style: italic; 203 | } 204 | 205 | 206 | /** 207 | * Add the correct background and color in IE 9-. 208 | */ 209 | 210 | mark { 211 | color: #000; 212 | background-color: #ff0; 213 | } 214 | 215 | 216 | /** 217 | * Add the correct font size in all browsers. 218 | */ 219 | 220 | small { 221 | font-size: 80%; 222 | } 223 | 224 | 225 | /** 226 | * Prevent `sub` and `sup` elements from affecting the line height in 227 | * all browsers. 228 | */ 229 | 230 | sub, 231 | sup { 232 | font-size: 75%; 233 | line-height: 0; 234 | position: relative; 235 | vertical-align: baseline; 236 | } 237 | 238 | sub { 239 | bottom: -.25em; 240 | } 241 | 242 | sup { 243 | top: -.5em; 244 | } 245 | 246 | 247 | /* Embedded content 248 | ========================================================================== */ 249 | 250 | 251 | /** 252 | * Add the correct display in IE 9-. 253 | */ 254 | 255 | audio, 256 | video { 257 | display: inline-block; 258 | } 259 | 260 | 261 | /** 262 | * Add the correct display in iOS 4-7. 263 | */ 264 | 265 | audio:not([controls]) { 266 | display: none; 267 | height: 0; 268 | } 269 | 270 | 271 | /** 272 | * Remove the border on images inside links in IE 10-. 273 | */ 274 | 275 | img { 276 | border-style: none; 277 | } 278 | 279 | 280 | /** 281 | * Hide the overflow in IE. 282 | */ 283 | 284 | svg:not(:root) { 285 | overflow: hidden; 286 | } 287 | 288 | 289 | /* Forms 290 | ========================================================================== */ 291 | 292 | 293 | /** 294 | * 1. Change the font styles in all browsers (opinionated). 295 | * 2. Remove the margin in Firefox and Safari. 296 | */ 297 | 298 | button, 299 | input, 300 | optgroup, 301 | select, 302 | textarea { 303 | font-family: sans-serif; 304 | /* 1 */ 305 | font-size: 100%; 306 | /* 1 */ 307 | line-height: 1.15; 308 | /* 1 */ 309 | margin: 0; 310 | /* 2 */ 311 | } 312 | 313 | 314 | /** 315 | * Show the overflow in IE. 316 | * 1. Show the overflow in Edge. 317 | */ 318 | 319 | button, 320 | input { 321 | /* 1 */ 322 | overflow: visible; 323 | } 324 | 325 | 326 | /** 327 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 328 | * 1. Remove the inheritance of text transform in Firefox. 329 | */ 330 | 331 | button, 332 | select { 333 | /* 1 */ 334 | text-transform: none; 335 | } 336 | 337 | 338 | /** 339 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 340 | * controls in Android 4. 341 | * 2. Correct the inability to style clickable types in iOS and Safari. 342 | */ 343 | 344 | button, 345 | html [type='button'], 346 | 347 | /* 1 */ 348 | 349 | [type='reset'], 350 | [type='submit'] { 351 | -webkit-appearance: button; 352 | /* 2 */ 353 | } 354 | 355 | 356 | /** 357 | * Remove the inner border and padding in Firefox. 358 | */ 359 | 360 | [type='button']::-moz-focus-inner, 361 | [type='reset']::-moz-focus-inner, 362 | [type='submit']::-moz-focus-inner, 363 | button::-moz-focus-inner { 364 | padding: 0; 365 | border-style: none; 366 | } 367 | 368 | 369 | /** 370 | * Restore the focus styles unset by the previous rule. 371 | */ 372 | 373 | [type='button']:-moz-focusring, 374 | [type='reset']:-moz-focusring, 375 | [type='submit']:-moz-focusring, 376 | button:-moz-focusring { 377 | outline: 1px dotted ButtonText; 378 | } 379 | 380 | 381 | /** 382 | * Change the border, margin, and padding in all browsers (opinionated). 383 | */ 384 | 385 | fieldset { 386 | margin: 0 2px; 387 | padding: .35em .625em .75em; 388 | border: 1px solid #c0c0c0; 389 | } 390 | 391 | 392 | /** 393 | * 1. Correct the text wrapping in Edge and IE. 394 | * 2. Correct the color inheritance from `fieldset` elements in IE. 395 | * 3. Remove the padding so developers are not caught out when they zero out 396 | * `fieldset` elements in all browsers. 397 | */ 398 | 399 | legend { 400 | display: table; 401 | /* 1 */ 402 | box-sizing: border-box; 403 | /* 1 */ 404 | max-width: 100%; 405 | /* 1 */ 406 | padding: 0; 407 | /* 3 */ 408 | white-space: normal; 409 | /* 1 */ 410 | color: inherit; 411 | /* 2 */ 412 | } 413 | 414 | 415 | /** 416 | * 1. Add the correct display in IE 9-. 417 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 418 | */ 419 | 420 | progress { 421 | display: inline-block; 422 | /* 1 */ 423 | vertical-align: baseline; 424 | /* 2 */ 425 | } 426 | 427 | 428 | /** 429 | * Remove the default vertical scrollbar in IE. 430 | */ 431 | 432 | textarea { 433 | overflow: auto; 434 | } 435 | 436 | 437 | /** 438 | * 1. Add the correct box sizing in IE 10-. 439 | * 2. Remove the padding in IE 10-. 440 | */ 441 | 442 | [type='checkbox'], 443 | [type='radio'] { 444 | box-sizing: border-box; 445 | /* 1 */ 446 | padding: 0; 447 | /* 2 */ 448 | } 449 | 450 | 451 | /** 452 | * Correct the cursor style of increment and decrement buttons in Chrome. 453 | */ 454 | 455 | [type='number']::-webkit-inner-spin-button, 456 | [type='number']::-webkit-outer-spin-button { 457 | height: auto; 458 | } 459 | 460 | 461 | /** 462 | * 1. Correct the odd appearance in Chrome and Safari. 463 | * 2. Correct the outline style in Safari. 464 | */ 465 | 466 | [type='search'] { 467 | outline-offset: -2px; 468 | /* 2 */ 469 | -webkit-appearance: textfield; 470 | /* 1 */ 471 | } 472 | 473 | 474 | /** 475 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 476 | */ 477 | 478 | [type='search']::-webkit-search-cancel-button, 479 | [type='search']::-webkit-search-decoration { 480 | -webkit-appearance: none; 481 | } 482 | 483 | 484 | /** 485 | * 1. Correct the inability to style clickable types in iOS and Safari. 486 | * 2. Change font properties to `inherit` in Safari. 487 | */ 488 | 489 | ::-webkit-file-upload-button { 490 | font: inherit; 491 | /* 2 */ 492 | -webkit-appearance: button; 493 | /* 1 */ 494 | } 495 | 496 | 497 | /* Interactive 498 | ========================================================================== */ 499 | 500 | 501 | /* 502 | * Add the correct display in IE 9-. 503 | * 1. Add the correct display in Edge, IE, and Firefox. 504 | */ 505 | 506 | details, 507 | 508 | /* 1 */ 509 | 510 | menu { 511 | display: block; 512 | } 513 | 514 | 515 | /* 516 | * Add the correct display in all browsers. 517 | */ 518 | 519 | summary { 520 | display: list-item; 521 | } 522 | 523 | 524 | /* Scripting 525 | ========================================================================== */ 526 | 527 | 528 | /** 529 | * Add the correct display in IE 9-. 530 | */ 531 | 532 | canvas { 533 | display: inline-block; 534 | } 535 | 536 | 537 | /** 538 | * Add the correct display in IE. 539 | */ 540 | 541 | template { 542 | display: none; 543 | } 544 | 545 | 546 | /* Hidden 547 | ========================================================================== */ 548 | 549 | 550 | /** 551 | * Add the correct display in IE 10-. 552 | */ 553 | 554 | [hidden] { 555 | display: none; 556 | } 557 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LingRay/mindmap/711f488a0b302260607583e40adb3676b4f2e7a8/static/.gitkeep -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/getingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | var server = require('../../build/dev-server.js') 4 | 5 | // 2. run the nightwatch test suite against it 6 | // to run in additional browsers: 7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 8 | // 2. add it to the --env flag below 9 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 10 | // For more information on Nightwatch's config file, see 11 | // http://nightwatchjs.org/guide#settings-file 12 | var opts = process.argv.slice(2) 13 | if (opts.indexOf('--config') === -1) { 14 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 15 | } 16 | if (opts.indexOf('--env') === -1) { 17 | opts = opts.concat(['--env', 'chrome']) 18 | } 19 | 20 | var spawn = require('cross-spawn') 21 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 22 | 23 | runner.on('exit', function (code) { 24 | server.close() 25 | process.exit(code) 26 | }) 27 | 28 | runner.on('error', function (err) { 29 | server.close() 30 | throw err 31 | }) 32 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.productionTip = false 3 | 4 | // Polyfill fn.bind() for PhantomJS 5 | /* eslint-disable no-extend-native */ 6 | Function.prototype.bind = require('function-bind') 7 | 8 | // require all test files (files that ends with .spec.js) 9 | const testsContext = require.context('./specs', true, /\.spec$/) 10 | testsContext.keys().forEach(testsContext) 11 | 12 | // require all src files except main.js for coverage. 13 | // you can also change this to match only the subset of files that 14 | // you want coverage for. 15 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 16 | srcContext.keys().forEach(srcContext) 17 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from '@/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(Hello) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | --------------------------------------------------------------------------------