├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmrc ├── .nvmrc ├── .postcssrc.js ├── LICENSE ├── README.md ├── 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 ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── demo ├── App.vue ├── main.js └── test │ ├── dev-test.vue │ ├── i18n │ ├── en-US.js │ ├── index.js │ ├── zh-CN.js │ └── zh-TW.js │ ├── ms-test.vue │ └── test-plugin.vue ├── index.html ├── package-lock.json ├── package.json └── src ├── assets └── minder │ ├── iconpriority.png │ ├── iconprogress.png │ ├── icons.png │ └── mold.png ├── components ├── main │ ├── footer.vue │ ├── header.vue │ ├── mainEditor.vue │ └── navigator.vue ├── menu │ ├── edit │ │ ├── attachment.vue │ │ ├── editDel.vue │ │ ├── editMenu.vue │ │ ├── expand.vue │ │ ├── insertBox.vue │ │ ├── moveBox.vue │ │ ├── progressBox.vue │ │ ├── selection.vue │ │ ├── sequenceBox.vue │ │ └── tagBox.vue │ └── view │ │ ├── arrange.vue │ │ ├── fontOperation.vue │ │ ├── mold.vue │ │ ├── styleOperation.vue │ │ ├── theme.vue │ │ └── viewMenu.vue └── minderEditor.vue ├── index.js ├── locale ├── format.js ├── index.js └── lang │ ├── en-US.js │ ├── zh-CN.js │ └── zh-TW.js ├── mixins.js ├── mixins └── locale.js ├── props.js ├── script ├── editor.js ├── expose-editor.js ├── hotbox.js ├── lang.js ├── minder.js ├── protocol │ ├── freemind.js │ ├── json.js │ ├── markdown.js │ ├── plain.js │ ├── png.js │ ├── svg.js │ └── xmind.js ├── runtime │ ├── clipboard-mimetype.js │ ├── clipboard.js │ ├── container.js │ ├── drag.js │ ├── exports.js │ ├── fsm.js │ ├── history.js │ ├── hotbox.js │ ├── input.js │ ├── jumping.js │ ├── minder.js │ ├── node.js │ ├── priority.js │ ├── progress.js │ ├── receiver.js │ └── tag.js ├── store.js └── tool │ ├── debug.js │ ├── format.js │ ├── innertext.js │ ├── key.js │ ├── keymap.js │ └── utils.js └── style ├── dropdown-list.scss ├── editor.scss ├── header.scss ├── hotbox.scss ├── mixin.scss ├── navigator.scss └── normalize.css /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | "> 1%", 10 | "last 2 versions", 11 | "not ie <= 8" 12 | ] 13 | } 14 | } 15 | ], 16 | "stage-2", 17 | [ 18 | "es2015", 19 | { 20 | "modules": false 21 | } 22 | ] 23 | ], 24 | "plugins": [ 25 | "transform-vue-jsx", 26 | "syntax-dynamic-import" 27 | ], 28 | "env": { 29 | "test": { 30 | "presets": [ 31 | "env", 32 | "stage-2" 33 | ], 34 | "plugins": [ 35 | "istanbul" 36 | ] 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/unit/coverage 5 | test/e2e/reports 6 | selenium-debug.log 7 | /.idea/ 8 | dist 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | message="Chore(release): %s" -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": { 3 | "autoprefixer": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, 刘毅 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const ora = require('ora') 6 | const rm = require('rimraf') 7 | const path = require('path') 8 | const chalk = require('chalk') 9 | const webpack = require('webpack') 10 | const config = require('../config') 11 | const webpackConfig = require('./webpack.prod.conf') 12 | 13 | const 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, (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 | if (stats.hasErrors()) { 30 | console.log(chalk.red(' Build failed with errors.\n')) 31 | process.exit(1) 32 | } 33 | 34 | console.log(chalk.cyan(' Build complete.\n')) 35 | console.log(chalk.yellow( 36 | ' Tip: built files are meant to be served over an HTTP server.\n' + 37 | ' Opening index.html over file:// won\'t work.\n' 38 | )) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 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 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | const config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | const opn = require('opn') 9 | const path = require('path') 10 | const os = require('os') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = require('./webpack.dev.conf') 15 | const port = process.env.PORT || config.dev.port 16 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 17 | const proxyTable = config.dev.proxyTable 18 | const app = express() 19 | const compiler = webpack(webpackConfig) 20 | const chalk = require('chalk') 21 | 22 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 23 | publicPath: webpackConfig.output.publicPath, 24 | quiet: true 25 | }) 26 | 27 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 28 | log: () => {} 29 | }) 30 | compiler.hooks.compilation.tap('WebpackTranslationPlugin', (compilation) => { 31 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 32 | hotMiddleware.publish({ action: 'reload' }) 33 | if (typeof cb == "function") { 34 | cb() 35 | } 36 | }) 37 | }) 38 | 39 | Object.keys(proxyTable).forEach(function (context) { 40 | let options = proxyTable[context] 41 | if (typeof options === 'string') { 42 | options = { target: options } 43 | } 44 | app.use(proxyMiddleware(options.filter || context, options)) 45 | }) 46 | 47 | app.use(require('connect-history-api-fallback')()) 48 | app.use(devMiddleware) 49 | app.use(hotMiddleware) 50 | 51 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 52 | app.use(staticPath, express.static('./static')) 53 | 54 | let networkIP = '' 55 | try { 56 | networkIP = os.networkInterfaces().en0[1].address 57 | } catch (e) { 58 | networkIP = ''; 59 | } 60 | 61 | let _resolve 62 | const readyPromise = new Promise(resolve => { 63 | _resolve = resolve 64 | }) 65 | 66 | const localhostURL = `http://localhost:${port}` 67 | const networkURL = `http://${networkIP}:${port}` 68 | console.log(chalk.green('> 正在启动本地服务...')) 69 | devMiddleware.waitUntilValid(() => { 70 | console.log('\n App running at:') 71 | console.log(` - Local: ${chalk.cyan(localhostURL)}`) 72 | console.log(` - Network: ${chalk.cyan(networkURL)}\n`) 73 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 74 | opn(localhostURL) 75 | } 76 | _resolve() 77 | }) 78 | 79 | const server = app.listen(port) 80 | 81 | module.exports = { 82 | ready: readyPromise, 83 | close: () => { 84 | server.close() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const packageConfig = require('../package.json') 5 | 6 | exports.assetsPath = function (_path) { 7 | var assetsSubDirectory = process.env.NODE_ENV === 'production' ? 8 | config.build.assetsSubDirectory : 9 | config.dev.assetsSubDirectory 10 | return path.posix.join(assetsSubDirectory, _path) 11 | }, 12 | 13 | exports.createNotifierCallback = () => { 14 | const notifier = require('node-notifier') 15 | 16 | return (severity, errors) => { 17 | if (severity !== 'error') return 18 | 19 | const error = errors[0] 20 | const filename = error.file && error.file.split('!').pop() 21 | 22 | notifier.notify({ 23 | title: packageConfig.name, 24 | message: severity + ': ' + error.name, 25 | subtitle: filename || '', 26 | icon: path.join(__dirname, 'logo.png') 27 | }) 28 | } 29 | } 30 | 31 | exports.cssLoaders = function (options) { 32 | options = options || {} 33 | 34 | var cssLoader = { 35 | loader: 'css-loader', 36 | options: { 37 | minimize: process.env.NODE_ENV === 'production', 38 | sourceMap: options.sourceMap 39 | } 40 | } 41 | 42 | function generateLoaders(loader, loaderOptions) { 43 | var loaders = [cssLoader] 44 | if (loader) { 45 | loaders.push({ 46 | loader: loader + '-loader', 47 | options: Object.assign({}, loaderOptions, { 48 | sourceMap: options.sourceMap 49 | }) 50 | }) 51 | } 52 | 53 | if (options.extract) { 54 | return [MiniCssExtractPlugin.loader].concat(loaders) 55 | } else { 56 | return ['vue-style-loader'].concat(loaders) 57 | } 58 | } 59 | 60 | return { 61 | css: generateLoaders(), 62 | postcss: generateLoaders(), 63 | scss: generateLoaders('sass'), 64 | } 65 | } 66 | 67 | exports.styleLoaders = function (options) { 68 | var output = [] 69 | var loaders = exports.cssLoaders(options) 70 | for (var extension in loaders) { 71 | var loader = loaders[extension] 72 | output.push({ 73 | test: new RegExp('\\.' + extension + '$'), 74 | use: loader 75 | }) 76 | } 77 | return output 78 | } 79 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils') 2 | const config = require('../config') 3 | const isProduction = process.env.NODE_ENV === 'production' 4 | const sourceMapEnabled = isProduction 5 | ? config.build.productionSourceMap 6 | : config.dev.cssSourceMap 7 | 8 | module.exports = { 9 | loaders: utils.cssLoaders({ 10 | sourceMap: sourceMapEnabled, 11 | extract: isProduction 12 | }), 13 | cssSourceMap: sourceMapEnabled, 14 | cacheBusting: config.dev.cacheBusting, 15 | transformToRequire: { 16 | video: ['src', 'poster'], 17 | source: 'src', 18 | img: 'src', 19 | image: 'xlink:href' 20 | } 21 | } -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const vueLoaderConfig = require('./vue-loader.conf') 5 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 6 | const webpack = require('webpack') 7 | 8 | function resolve(dir) { 9 | return path.join(__dirname, '..', dir) 10 | } 11 | 12 | module.exports = { 13 | context: path.resolve(__dirname, '../'), 14 | entry: { 15 | app: './demo/main.js' 16 | }, 17 | output: { 18 | path: config.build.assetsRoot, 19 | filename: '[name].js', 20 | globalObject: "this", 21 | publicPath: process.env.NODE_ENV === 'production' ? 22 | config.build.assetsPublicPath : config.dev.assetsPublicPath 23 | }, 24 | resolve: { 25 | extensions: ['*', '.js', '.vue', '.json'], 26 | alias: { 27 | 'vue$': 'vue/dist/vue.esm.js', 28 | '@': resolve('src') 29 | } 30 | }, 31 | module: { 32 | rules: [{ 33 | test: /\.vue$/, 34 | loader: 'vue-loader', 35 | options: vueLoaderConfig 36 | }, 37 | { 38 | test: /\.js$/, 39 | loader: 'babel-loader', 40 | include: [ 41 | resolve('src'), 42 | resolve('test'), 43 | resolve('node_modules/element-ui/packages'), 44 | resolve('node_modules/element-ui/src'), 45 | resolve('node_modules/element-ui/src'), 46 | resolve('node_modules/hotbox-minder/src'), 47 | resolve('node_modules/@7polo/kity/src'), 48 | resolve('node_modules/@7polo/kityminder-core/src'), 49 | ] 50 | }, 51 | { 52 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 53 | loader: 'url-loader', 54 | options: { 55 | limit: 40000, 56 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 57 | } 58 | }, 59 | { 60 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 61 | loader: 'url-loader', 62 | options: { 63 | limit: 40000, 64 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 65 | } 66 | }, 67 | { 68 | test: /\.scss$/, 69 | loaders: ["style-loader", "css-loader", "sass-loader"] 70 | }, 71 | { 72 | test: /.md$/, 73 | loader: "text-loader" 74 | } 75 | ] 76 | }, 77 | plugins: [ 78 | new VueLoaderPlugin(), 79 | ], 80 | node: { 81 | setImmediate: false, 82 | dgram: 'empty', 83 | fs: 'empty', 84 | net: 'empty', 85 | tls: 'empty', 86 | child_process: 'empty' 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils') 2 | const webpack = require('webpack') 3 | const config = require('../config') 4 | const merge = require('webpack-merge') 5 | const path = require('path') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | const HOST = process.env.HOST 11 | const PORT = process.env.PORT && Number(process.env.PORT) 12 | 13 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 14 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 15 | }) 16 | 17 | module.exports = merge(baseWebpackConfig, { 18 | mode: 'development', 19 | module: { 20 | rules: [{ 21 | test: /\.less$/, 22 | use: [ 23 | 'vue-style-loader', 24 | { 25 | loader: 'css-loader', 26 | options: { 27 | sourceMap: config.dev.cssSourceMap 28 | } 29 | }, 30 | { 31 | loader: 'less-loader' 32 | }, 33 | 'postcss-loader' 34 | ] 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: [ 39 | 'vue-style-loader', 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | importLoaders: 1, 44 | sourceMap: config.dev.cssSourceMap 45 | } 46 | }, 47 | 'postcss-loader' 48 | ] 49 | } 50 | ] 51 | }, 52 | devtool: config.dev.devtool, 53 | devServer: { 54 | clientLogLevel: 'warning', 55 | historyApiFallback: { 56 | rewrites: [{ 57 | from: /.*/, 58 | to: path.posix.join(config.dev.assetsPublicPath, 'index.html') 59 | },], 60 | }, 61 | hot: true, 62 | contentBase: false, 63 | compress: true, 64 | host: HOST || config.dev.host, 65 | port: PORT || config.dev.port, 66 | open: config.dev.autoOpenBrowser, 67 | overlay: config.dev.errorOverlay ? 68 | { 69 | errors: true 70 | } : 71 | false, 72 | publicPath: config.dev.assetsPublicPath, 73 | proxy: config.dev.proxyTable, 74 | quiet: true // necessary for FriendlyErrorsPlugin 75 | }, 76 | watchOptions: { 77 | ignored: /node_modules/, 78 | poll: 1000, //每秒钟询问变化次数,建议设置1000 79 | aggregateTimeout: 500 //累计的超时 80 | }, 81 | optimization: { 82 | noEmitOnErrors: true, 83 | namedModules: true 84 | }, 85 | plugins: [ 86 | new webpack.DefinePlugin({ 87 | 'process.env': config.dev.env 88 | }), 89 | new webpack.HotModuleReplacementPlugin(), 90 | new webpack.NoEmitOnErrorsPlugin(), 91 | new HtmlWebpackPlugin({ 92 | filename: 'index.html', 93 | template: 'index.html', 94 | inject: true 95 | }), 96 | new FriendlyErrorsPlugin(), 97 | ] 98 | }) 99 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 8 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 9 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 10 | 11 | const env = config.build.env 12 | 13 | const webpackConfig = merge(baseWebpackConfig, { 14 | mode: 'production', 15 | // mode: "development", 16 | entry: './src/index.js', 17 | output: { 18 | path: config.build.assetsRoot, 19 | publicPath: '/dist/', 20 | filename: utils.assetsPath('vue-minder-editor-extended.js'), 21 | chunkFilename: utils.assetsPath('[name].[chunkhash].js'), 22 | library: 'vueMinderEditorExtended', 23 | libraryTarget: 'umd', 24 | umdNamedDefine: true, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.css$/, 30 | use: [ 31 | 'vue-style-loader', 32 | 'css-loader', 33 | 'postcss-loader' 34 | ] 35 | } 36 | ] 37 | }, 38 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 39 | performance: { 40 | hints: false, // 关闭性能提示 41 | }, 42 | optimization: { 43 | }, 44 | plugins: [ 45 | new webpack.ProgressPlugin(), 46 | new webpack.DefinePlugin({ 47 | 'process.env': env 48 | }), 49 | new webpack.HashedModuleIdsPlugin(), 50 | new webpack.optimize.ModuleConcatenationPlugin(), 51 | new UglifyJsPlugin({ 52 | uglifyOptions: { 53 | show_copyright: false, 54 | comments: false, 55 | compress: { 56 | drop_debugger: true, 57 | drop_console: false 58 | } 59 | }, 60 | sourceMap: config.build.productionSourceMap, 61 | parallel: true 62 | }), 63 | new OptimizeCSSPlugin({ 64 | cssProcessorOptions: config.build.productionSourceMap 65 | ? { safe: true, map: { inline: false } } 66 | : { safe: true } 67 | }), 68 | new MiniCssExtractPlugin({ 69 | filename: 'focus.index.[contenthash:8].css' 70 | }), 71 | ] 72 | }) 73 | 74 | if (config.build.productionGzip) { 75 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 76 | webpackConfig.plugins.push( 77 | new CompressionWebpackPlugin({ 78 | asset: '[path].gz[query]', 79 | algorithm: 'gzip', 80 | test: new RegExp( 81 | '\\.(' + 82 | config.build.productionGzipExtensions.join('|') + 83 | ')$' 84 | ), 85 | threshold: 10240, 86 | minRatio: 0.8 87 | }) 88 | ) 89 | } 90 | 91 | if (config.build.bundleAnalyzerReport) { 92 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 93 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 94 | } 95 | 96 | module.exports = webpackConfig 97 | -------------------------------------------------------------------------------- /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 | var path = require('path') 2 | 3 | module.exports = { 4 | build: { 5 | env: require('./prod.env'), 6 | index: path.resolve(__dirname, '../dist/index.html'), 7 | assetsRoot: path.resolve(__dirname, '../dist'), 8 | assetsSubDirectory: 'static', 9 | assetsPublicPath: '/', 10 | productionSourceMap: true, 11 | productionGzip: true, 12 | productionGzipExtensions: ['js', 'css'], 13 | bundleAnalyzerReport: process.env.npm_config_report, 14 | devtool: "eval" 15 | //devtool: 'cheap-module-eval-source-map' 16 | }, 17 | dev: { 18 | env: require('./dev.env'), 19 | port: 8088, 20 | autoOpenBrowser: true, 21 | assetsSubDirectory: 'static', 22 | assetsPublicPath: '/', 23 | proxyTable: {}, 24 | cssSourceMap: true, 25 | cacheBusting: true, 26 | devtool: 'cheap-module-eval-source-map', 27 | poll: false, 28 | errorOverlay: true, 29 | notifyOnErrors: true 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App'; 3 | import 'element-ui/lib/theme-chalk/index.css'; 4 | import ElementUI from 'element-ui'; 5 | import vueMinderEditorExtended from "../src/index" 6 | // 使用打包后的文件 7 | // import vueMinderEditorExtended from "../dist/static/vue-minder-editor-extended"; 8 | Vue.config.productionTip = true; 9 | 10 | // 方式一 11 | // import locale from '/src/locale/lang/en-US' 12 | // Vue.use(vueMinderEditorExtended, { 13 | // locale 14 | // }); 15 | 16 | // 方式二 17 | // import lang from '/src/locale/lang/en-US' 18 | // import locale from '/src/locale' 19 | // // 设置语言 20 | // locale.use(lang) 21 | // Vue.use(vueMinderEditorExtended); 22 | 23 | // 方式三 24 | import i18n from "./test/i18n/index"; 25 | Vue.use(vueMinderEditorExtended, { 26 | i18n: (key, value) => i18n.t(key, value) 27 | }); 28 | 29 | Vue.use(ElementUI, { 30 | i18n: (key, value) => i18n.t(key, value) 31 | }); 32 | 33 | new Vue({ 34 | el: '#app', 35 | template: '', 36 | components: { 37 | App 38 | }, 39 | i18n 40 | }) 41 | -------------------------------------------------------------------------------- /demo/test/dev-test.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 265 | 266 | 269 | -------------------------------------------------------------------------------- /demo/test/i18n/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en_US: 'English', 3 | zh_CN: 'Chinese simplified', 4 | zh_TW: 'Chinese traditional' 5 | } 6 | -------------------------------------------------------------------------------- /demo/test/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueI18n from "vue-i18n"; 3 | import enLocale from "element-ui/lib/locale/lang/en"; 4 | import zh_CNLocale from "element-ui/lib/locale/lang/zh-CN"; 5 | import zh_TWLocale from "element-ui/lib/locale/lang/zh-TW"; 6 | import zh_CN from "./zh-CN"; 7 | import en_US from "./en-US"; 8 | import zh_TW from "./zh-TW"; 9 | 10 | import minder_zh_CN from "../../../src/locale/lang/zh-CN"; 11 | import minder_en_US from "../../../src/locale/lang/en-US"; 12 | import minder_zh_TW from "../../../src/locale/lang/zh-TW"; 13 | 14 | export const CURRENT_LANGUAGE = 'current_language'; 15 | 16 | Vue.use(VueI18n); 17 | 18 | const messages = { 19 | 'en_US': { 20 | ...enLocale, 21 | ...en_US, 22 | ...minder_en_US 23 | }, 24 | 'zh_CN': { 25 | ...zh_CNLocale, 26 | ...zh_CN, 27 | ...minder_zh_CN 28 | 29 | }, 30 | 'zh_TW': { 31 | ...zh_TWLocale, 32 | ...zh_TW, 33 | ...minder_zh_TW 34 | 35 | } 36 | }; 37 | 38 | const index = new VueI18n({ 39 | locale: 'zh_CN', 40 | messages, 41 | silentTranslationWarn: true 42 | }); 43 | 44 | const loadedLanguages = ['en_US', 'zh_CN', 'zh_TW']; 45 | 46 | function setI18nLanguage(lang) { 47 | index.locale = lang; 48 | document.querySelector('html').setAttribute('lang', lang); 49 | localStorage.setItem(CURRENT_LANGUAGE, lang); 50 | return lang; 51 | } 52 | 53 | Vue.prototype.$setLang = function (lang) { 54 | if (index.locale !== lang) { 55 | if (!loadedLanguages.includes(lang)) { 56 | let file = lang.replace("_", "-"); 57 | return import(`./${file}`).then(response => { 58 | index.mergeLocaleMessage(lang, response.default); 59 | loadedLanguages.push(lang); 60 | return setI18nLanguage(lang) 61 | }) 62 | } 63 | return Promise.resolve(setI18nLanguage(lang)) 64 | } 65 | return Promise.resolve(lang) 66 | }; 67 | 68 | export default index; 69 | -------------------------------------------------------------------------------- /demo/test/i18n/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en_US: '英语', 3 | zh_CN: '中文简体', 4 | zh_TW: '中文繁体' 5 | } 6 | -------------------------------------------------------------------------------- /demo/test/i18n/zh-TW.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en_US: '英語', 3 | zh_CN: '中文簡體', 4 | zh_TW: '中文繁體' 5 | } 6 | -------------------------------------------------------------------------------- /demo/test/ms-test.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /demo/test/test-plugin.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 81 | 82 | 85 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue-Minder-Editor 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-minder-editor-extended", 3 | "version": "1.3.3", 4 | "description": "基于 Vue2 的脑图编辑组件", 5 | "author": "Lruihao (https://lruihao.cn)", 6 | "original": "AgAngle <1323481023@qq.com>", 7 | "license": "BSD 3-Clause", 8 | "scripts": { 9 | "dev": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 12 | "e2e": "node test/e2e/runner.js", 13 | "test": "npm run unit && npm run e2e", 14 | "version": "npm run build && npm publish" 15 | }, 16 | "main": "dist/static/vue-minder-editor-extended.js", 17 | "dependencies": { 18 | "@7polo/kity": "^2.0.8", 19 | "@cell-x/kityminder-core": "^1.4.52", 20 | "element-ui": "^2.15.14", 21 | "hotbox-minder": "^1.0.15", 22 | "vue": "2.6.14", 23 | "vue-i18n": "^8.28.2" 24 | }, 25 | "devDependencies": { 26 | "acorn": "^6.4.2", 27 | "assets-webpack-plugin": "^3.9.12", 28 | "autoprefixer": "^6.7.7", 29 | "babel-core": "^6.26.3", 30 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 31 | "babel-loader": "^7.1.5", 32 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 33 | "babel-plugin-syntax-jsx": "^6.18.0", 34 | "babel-plugin-transform-imports": "1.5.0", 35 | "babel-plugin-transform-vue-jsx": "^3.7.0", 36 | "babel-polyfill": "^6.26.0", 37 | "babel-preset-env": "^1.7.0", 38 | "babel-preset-es2015": "^6.24.1", 39 | "babel-preset-stage-2": "^6.24.1", 40 | "babel-register": "^6.26.0", 41 | "chai": "^3.5.0", 42 | "chalk": "^1.1.3", 43 | "compression-webpack-plugin": "^1.1.12", 44 | "connect-history-api-fallback": "^1.6.0", 45 | "cross-env": "^3.2.4", 46 | "cross-spawn": "^5.1.0", 47 | "css-loader": "^0.28.11", 48 | "eslint": "^5.16.0", 49 | "eslint-config-standard": "^12.0.0", 50 | "eslint-plugin-import": "^2.31.0", 51 | "eslint-plugin-node": "^8.0.1", 52 | "eslint-plugin-promise": "^4.3.1", 53 | "eslint-plugin-standard": "^4.1.0", 54 | "eventsource-polyfill": "^0.9.6", 55 | "express": "^4.21.2", 56 | "extract-zip": "^1.7.0", 57 | "file-loader": "^2.0.0", 58 | "friendly-errors-webpack-plugin": "^1.7.0", 59 | "function-bind": "^1.1.2", 60 | "html-loader": "^0.5.5", 61 | "html-webpack-plugin": "^3.2.0", 62 | "http-proxy-middleware": "^0.17.4", 63 | "inject-loader": "^2.0.1", 64 | "lolex": "^1.6.0", 65 | "mini-css-extract-plugin": "1.6.2", 66 | "nightwatch": "^0.9.21", 67 | "opn": "^4.0.2", 68 | "optimize-css-assets-webpack-plugin": "^1.3.2", 69 | "ora": "^1.4.0", 70 | "phantomjs-prebuilt": "^2.1.16", 71 | "postcss-import": "^12.0.1", 72 | "postcss-loader": "^3.0.0", 73 | "postcss-url": "^8.0.0", 74 | "rimraf": "^2.7.1", 75 | "sass": "~1.32.6", 76 | "sass-loader": "^10.5.2", 77 | "semver": "^5.7.2", 78 | "shelljs": "^0.8.5", 79 | "sinon": "^1.17.7", 80 | "sinon-chai": "^2.14.0", 81 | "style-loader": "^0.21.0", 82 | "text-loader": "0.0.1", 83 | "uglifyjs-webpack-plugin": "^2.2.0", 84 | "url-loader": "^0.5.9", 85 | "vue-loader": "^15.11.1", 86 | "vue-style-loader": "^2.0.5", 87 | "vue-template-compiler": "2.6.14", 88 | "webpack": "^4.47.0", 89 | "webpack-bundle-analyzer": "^3.9.0", 90 | "webpack-cli": "^3.3.12", 91 | "webpack-dev-middleware": "^3.7.3", 92 | "webpack-dev-server": "^3.11.3", 93 | "webpack-hot-middleware": "^2.26.1", 94 | "webpack-merge": "^4.2.2" 95 | }, 96 | "engines": { 97 | "node": ">= 14 < 20", 98 | "npm": ">= 9" 99 | }, 100 | "browserslist": [ 101 | "> 1%", 102 | "last 2 versions", 103 | "not ie <= 8" 104 | ], 105 | "keywords": [ 106 | "vue", 107 | "minder", 108 | "editor" 109 | ], 110 | "repository": { 111 | "type": "git", 112 | "url": "https://github.com/Lruihao/vue-minder-editor-extended.git" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/assets/minder/iconpriority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lruihao/vue-minder-editor-extended/98f321f3b2f087fa92173f8e6735e24e5f6cf018/src/assets/minder/iconpriority.png -------------------------------------------------------------------------------- /src/assets/minder/iconprogress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lruihao/vue-minder-editor-extended/98f321f3b2f087fa92173f8e6735e24e5f6cf018/src/assets/minder/iconprogress.png -------------------------------------------------------------------------------- /src/assets/minder/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lruihao/vue-minder-editor-extended/98f321f3b2f087fa92173f8e6735e24e5f6cf018/src/assets/minder/icons.png -------------------------------------------------------------------------------- /src/assets/minder/mold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lruihao/vue-minder-editor-extended/98f321f3b2f087fa92173f8e6735e24e5f6cf018/src/assets/minder/mold.png -------------------------------------------------------------------------------- /src/components/main/footer.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /src/components/main/header.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 88 | 89 | 92 | -------------------------------------------------------------------------------- /src/components/main/mainEditor.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 171 | 172 | 175 | 176 | 186 | -------------------------------------------------------------------------------- /src/components/menu/edit/attachment.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 122 | -------------------------------------------------------------------------------- /src/components/menu/edit/editDel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 86 | -------------------------------------------------------------------------------- /src/components/menu/edit/editMenu.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 60 | -------------------------------------------------------------------------------- /src/components/menu/edit/expand.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /src/components/menu/edit/insertBox.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 73 | -------------------------------------------------------------------------------- /src/components/menu/edit/moveBox.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 62 | -------------------------------------------------------------------------------- /src/components/menu/edit/progressBox.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 88 | -------------------------------------------------------------------------------- /src/components/menu/edit/selection.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 131 | -------------------------------------------------------------------------------- /src/components/menu/edit/sequenceBox.vue: -------------------------------------------------------------------------------- 1 |  26 | 27 | 98 | 188 | -------------------------------------------------------------------------------- /src/components/menu/edit/tagBox.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/menu/view/arrange.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | -------------------------------------------------------------------------------- /src/components/menu/view/fontOperation.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 292 | -------------------------------------------------------------------------------- /src/components/menu/view/mold.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 72 | 73 | 79 | -------------------------------------------------------------------------------- /src/components/menu/view/styleOperation.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 70 | -------------------------------------------------------------------------------- /src/components/menu/view/theme.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 91 | -------------------------------------------------------------------------------- /src/components/menu/view/viewMenu.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 47 | 48 | 50 | -------------------------------------------------------------------------------- /src/components/minderEditor.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 90 | 91 | 93 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import mindEditor from './components/minderEditor'; 2 | import * as locale from "./locale"; 3 | import PackageJSON from "../package.json"; 4 | require('@7polo/kity/dist/kity.js'); 5 | require('hotbox-minder/hotbox.js'); 6 | require('@cell-x/kityminder-core'); 7 | require('./script/expose-editor.js'); 8 | 9 | const install = function (Vue, options = {}) { 10 | locale.use(options.locale); 11 | locale.i18n(options.i18n); 12 | Vue.component(mindEditor.name, mindEditor); 13 | } 14 | 15 | const plugin = { 16 | name: "vueMinderEditorExtended", 17 | version: PackageJSON.version, 18 | locale: locale.use, 19 | i18n: locale.i18n, 20 | install, 21 | } 22 | 23 | if (typeof window !== 'undefined' && window.Vue) { 24 | window.Vue.use(plugin); 25 | } 26 | 27 | export default plugin; 28 | -------------------------------------------------------------------------------- /src/locale/format.js: -------------------------------------------------------------------------------- 1 | import { hasOwn } from 'element-ui/src/utils/util'; 2 | 3 | const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g; 4 | /** 5 | * String format template 6 | * - Inspired: 7 | * https://github.com/Matt-Esch/string-template/index.js 8 | */ 9 | export default function(Vue) { 10 | 11 | /** 12 | * template 13 | * 14 | * @param {String} string 15 | * @param {Array} ...args 16 | * @return {String} 17 | */ 18 | 19 | function template(string, ...args) { 20 | if (args.length === 1 && typeof args[0] === 'object') { 21 | args = args[0]; 22 | } 23 | 24 | if (!args || !args.hasOwnProperty) { 25 | args = {}; 26 | } 27 | 28 | return string.replace(RE_NARGS, (match, prefix, i, index) => { 29 | let result; 30 | 31 | if (string[index - 1] === '{' && 32 | string[index + match.length] === '}') { 33 | return i; 34 | } else { 35 | result = hasOwn(args, i) ? args[i] : null; 36 | if (result === null || result === undefined) { 37 | return ''; 38 | } 39 | 40 | return result; 41 | } 42 | }); 43 | } 44 | 45 | return template; 46 | } 47 | -------------------------------------------------------------------------------- /src/locale/index.js: -------------------------------------------------------------------------------- 1 | import defaultLang from './lang/zh-CN'; 2 | import Vue from 'vue'; 3 | import deepmerge from 'deepmerge'; 4 | import Format from './format'; 5 | 6 | 7 | const format = Format(Vue); 8 | let lang = defaultLang; 9 | let merged = false; 10 | 11 | let i18nHandler = function() { 12 | const vuei18n = Object.getPrototypeOf(this || Vue).$t; 13 | if (typeof vuei18n === 'function' && !!Vue.locale) { 14 | if (!merged) { 15 | merged = true; 16 | Vue.locale( 17 | Vue.config.lang, 18 | deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true }) 19 | ); 20 | } 21 | return vuei18n.apply(this, arguments); 22 | } 23 | }; 24 | 25 | export const t = function(path, options) { 26 | let value = i18nHandler.apply(this, arguments); 27 | if (value !== null && value !== undefined) return value; 28 | 29 | const array = path.split('.'); 30 | let current = lang; 31 | 32 | for (let i = 0, j = array.length; i < j; i++) { 33 | const property = array[i]; 34 | value = current[property]; 35 | if (!value) return ''; 36 | if (i === j - 1) return format(value, options); 37 | current = value; 38 | } 39 | return ''; 40 | }; 41 | 42 | export const use = function(l) { 43 | lang = l || lang; 44 | }; 45 | 46 | export const i18n = function(fn) { 47 | i18nHandler = fn || i18nHandler; 48 | }; 49 | 50 | export default { use, t, i18n}; 51 | -------------------------------------------------------------------------------- /src/locale/lang/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minder: { 3 | commons: { 4 | confirm: 'Confirm', 5 | clear: 'Clear', 6 | export: 'Export', 7 | cancel: 'Cancel', 8 | edit: 'Edit', 9 | delete: 'Delete', 10 | remove: 'Remove', 11 | return: 'Return', 12 | }, 13 | menu: { 14 | expand: { 15 | expand: 'Expand', 16 | folding: 'Folding', 17 | expand_one: 'Expand one level', 18 | expand_tow: 'Expand tow level', 19 | expand_three: 'Expand three level', 20 | expand_four: 'Expand four level', 21 | expand_five: 'Expand five level', 22 | expand_six: 'Expand six level' 23 | }, 24 | insert: { 25 | down: 'Subordinate', 26 | up: 'Superior', 27 | same: 'Same', 28 | _same: 'Same level', 29 | _down: 'Subordinate level', 30 | _up: 'Superior level', 31 | }, 32 | move: { 33 | up: 'Up', 34 | down: 'Down', 35 | forward: 'Forward', 36 | backward: 'Backward', 37 | }, 38 | progress: { 39 | progress: 'Progress', 40 | remove_progress: 'Remove progress', 41 | prepare: 'Prepare', 42 | complete_all: 'Complete all', 43 | complete: 'Complete', 44 | }, 45 | selection: { 46 | all: 'Select all', 47 | invert: 'Select invert', 48 | sibling: 'Select sibling node', 49 | same: 'Select same node', 50 | path: 'Select path', 51 | subtree: 'Select subtree', 52 | }, 53 | arrange: { 54 | arrange_layout: 'Arrange layout' 55 | }, 56 | font: { 57 | font: 'Font', 58 | size: 'Size' 59 | }, 60 | style: { 61 | clear: 'Clear style', 62 | copy: 'Copy style', 63 | paste: 'Paste style', 64 | } 65 | }, 66 | main: { 67 | header: { 68 | minder: 'Minder', 69 | style: 'Appearance style' 70 | }, 71 | main: { 72 | save: 'Save' 73 | }, 74 | navigator: { 75 | amplification: 'Amplification', 76 | narrow: 'Narrow', 77 | drag: 'Drag', 78 | locating_root: 'Locating root node', 79 | navigator: 'Navigator', 80 | }, 81 | history: { 82 | undo: 'Undo', 83 | redo: 'Redo' 84 | }, 85 | subject: { 86 | central: 'Central subject', 87 | branch: 'Subject' 88 | }, 89 | priority: 'Priority', 90 | tag: 'Tag' 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/locale/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minder: { 3 | commons: { 4 | confirm: '确定', 5 | clear: '清空', 6 | export: '导出', 7 | cancel: '取消', 8 | edit: '编辑', 9 | delete: '删除', 10 | remove: '移除', 11 | return: '返回', 12 | }, 13 | menu: { 14 | expand: { 15 | expand: '展开', 16 | folding: '收起', 17 | expand_one: '展开到一级节点', 18 | expand_tow: '展开到二级节点', 19 | expand_three: '展开到三级节点', 20 | expand_four: '展开到四级节点', 21 | expand_five: '展开到五级节点', 22 | expand_six: '展开到六级节点' 23 | }, 24 | insert: { 25 | down: '插入下级主题', 26 | up: '插入上级主题', 27 | same: '插入同级主题', 28 | _same: '同级', 29 | _down: '下级', 30 | _up: '上级', 31 | }, 32 | move: { 33 | up: '上移', 34 | down: '下移', 35 | forward: '前移', 36 | backward: '后移', 37 | }, 38 | progress: { 39 | progress: '进度', 40 | remove_progress: '移除进度', 41 | prepare: '未开始', 42 | complete_all: '全部完成', 43 | complete: '完成', 44 | }, 45 | selection: { 46 | all: '全选', 47 | invert: '反选', 48 | sibling: '选择兄弟节点', 49 | same: '选择同级节点', 50 | path: '选择路径', 51 | subtree: '选择子树', 52 | }, 53 | arrange: { 54 | arrange_layout: '整理布局' 55 | }, 56 | font: { 57 | font: '字体', 58 | size: '字号' 59 | }, 60 | theme: { 61 | classic: '脑图经典', 62 | 'classic-compact': '紧凑经典', 63 | 'fresh-blue': '天空蓝', 64 | 'fresh-blue-compat': '紧凑蓝', 65 | 'fresh-green': '文艺绿', 66 | 'fresh-green-compat': '紧凑绿', 67 | 'fresh-pink': '脑残粉', 68 | 'fresh-pink-compat': '紧凑粉', 69 | 'fresh-purple': '浪漫紫', 70 | 'fresh-purple-compat': '紧凑紫', 71 | 'fresh-red': '清新红', 72 | 'fresh-red-compat': '紧凑红', 73 | 'fresh-soil': '泥土黄', 74 | 'fresh-soil-compat': '紧凑黄', 75 | snow: '温柔冷光', 76 | 'snow-compact': '紧凑冷光', 77 | tianpan: '经典天盘', 78 | 'tianpan-compact': '紧凑天盘', 79 | fish: '鱼骨图', 80 | wire: '线框', 81 | 'custom': '自定义主题', 82 | 'custom-theme': '自定义主题', 83 | }, 84 | style: { 85 | clear: '清除样式', 86 | copy: '复制样式', 87 | paste: '粘贴样式', 88 | } 89 | }, 90 | main: { 91 | header: { 92 | minder: '思维导图', 93 | style: '外观样式' 94 | }, 95 | main: { 96 | save: '保存' 97 | }, 98 | navigator: { 99 | amplification: '放大', 100 | narrow: '缩小', 101 | drag: '拖拽', 102 | locating_root: '定位根节点', 103 | navigator: '导航器', 104 | }, 105 | history: { 106 | undo: '撤销', 107 | redo: '重做' 108 | }, 109 | subject: { 110 | central: '中心主题', 111 | branch: '分支主题' 112 | }, 113 | priority: '优先级', 114 | tag: '标签' 115 | } 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /src/locale/lang/zh-TW.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minder: { 3 | commons: { 4 | confirm: '確定', 5 | clear: '清空', 6 | export: '導出', 7 | cancel: '取消', 8 | edit: '編輯', 9 | delete: '刪除', 10 | remove: '移除', 11 | return: '返回', 12 | }, 13 | menu: { 14 | expand: { 15 | expand: '展開', 16 | folding: '收起', 17 | expand_one: '展開到一級節點', 18 | expand_tow: '展開到二級節點', 19 | expand_three: '展開到三級節點', 20 | expand_four: '展開到四級節點', 21 | expand_five: '展開到五級節點', 22 | expand_six: '展開到六級節點' 23 | }, 24 | insert: { 25 | down: '插入下級主題', 26 | up: '插入上級主題', 27 | same: '插入同級主題', 28 | _same: '同級', 29 | _down: '下級', 30 | _up: '上級', 31 | }, 32 | move: { 33 | up: '上移', 34 | down: '下移', 35 | forward: '前移', 36 | backward: '後移', 37 | }, 38 | progress: { 39 | progress: '進度', 40 | remove_progress: '移除進度', 41 | prepare: '未開始', 42 | complete_all: '全部完成', 43 | complete: '完成', 44 | }, 45 | selection: { 46 | all: '全選', 47 | invert: '反選', 48 | sibling: '選擇兄弟節點', 49 | same: '選擇同級節點', 50 | path: '選擇路徑', 51 | subtree: '選擇子樹', 52 | }, 53 | arrange: { 54 | arrange_layout: '整理布局' 55 | }, 56 | font: { 57 | font: '字體', 58 | size: '字號' 59 | }, 60 | style: { 61 | clear: '清除樣式', 62 | copy: '復製樣式', 63 | paste: '粘貼樣式', 64 | } 65 | }, 66 | main: { 67 | header: { 68 | minder: '思維導圖', 69 | style: '外觀樣式' 70 | }, 71 | main: { 72 | save: '保存' 73 | }, 74 | navigator: { 75 | amplification: '放大', 76 | narrow: '縮小', 77 | drag: '拖拽', 78 | locating_root: '定位根節點', 79 | navigator: '導航器', 80 | }, 81 | history: { 82 | undo: '撤銷', 83 | redo: '重做' 84 | }, 85 | subject: { 86 | central: '中心主題', 87 | branch: '分支主題' 88 | }, 89 | priority: '優先級', 90 | tag: '標簽' 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/mixins.js: -------------------------------------------------------------------------------- 1 | import Locale from "@/mixins/locale"; 2 | 3 | export default { 4 | ...Locale, 5 | computed: { 6 | operatorLabel() { 7 | for (let operator of this.operators) { 8 | if (operator.value === this.operator) { 9 | return this.t(operator.label) 10 | } 11 | } 12 | return this.operator 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/mixins/locale.js: -------------------------------------------------------------------------------- 1 | import { t } from '/src/locale'; 2 | 3 | export default { 4 | methods: { 5 | t(...args) { 6 | return t.apply(this, args); 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Api 列表 3 | */ 4 | 5 | export let mainEditorProps = { 6 | importJson: { 7 | type: Object, 8 | }, 9 | height: { 10 | type: Number, 11 | default: 500, 12 | }, 13 | // classic, classic-compact, snow, snow-compact, fresh-red, fresh-red-compat, 14 | // fresh-soil, fresh-soil-compat, fresh-green, fresh-green-compat, 15 | // fresh-blue, fresh-blue-compat, fresh-purple, fresh-purple-compat, 16 | // fresh-pink, fresh-pink-compat, fish, wire, tianpan, tianpan-compact 17 | theme: { 18 | type: String, 19 | default: 'fresh-blue', 20 | }, 21 | // 注册主题 22 | registerTheme: { 23 | type: Object, 24 | }, 25 | disabled: Boolean 26 | } 27 | 28 | export let priorityProps = { 29 | priorities: { 30 | // 自定义优先级 31 | type: Array, 32 | default() { 33 | return [] 34 | } 35 | }, 36 | priorityCount: { 37 | type: Number, 38 | default: 4, 39 | validator: function (value) { 40 | // 优先级最多支持 9 个级别 41 | return value <= 9; 42 | } 43 | }, 44 | priorityStartWithZero: { 45 | // 优先级是否从0开始 46 | type: Boolean, 47 | default: true 48 | }, 49 | priorityPrefix: { 50 | // 优先级显示的前缀 51 | type: String, 52 | default: 'P' 53 | }, 54 | priorityDisableCheck: Function, 55 | operators: [] 56 | } 57 | 58 | export let tagProps = { 59 | tags: { 60 | // 自定义标签 61 | type: Array, 62 | default() { 63 | return [] 64 | } 65 | }, 66 | distinctTags: { 67 | // 个别标签二选一 68 | type: Array, 69 | default() { 70 | return [] 71 | } 72 | }, 73 | tagDisableCheck: Function, 74 | tagEditCheck: Function 75 | } 76 | 77 | export let editMenuProps = { 78 | sequenceEnable: { 79 | type: Boolean, 80 | default: true 81 | }, 82 | tagEnable: { 83 | type: Boolean, 84 | default: true 85 | }, 86 | progressEnable: { 87 | type: Boolean, 88 | default: true 89 | }, 90 | moveEnable: { 91 | type: Boolean, 92 | default: true 93 | }, 94 | } 95 | 96 | export let moleProps = { 97 | // 默认样式 98 | defaultMold: { 99 | type: Number, 100 | default: 3 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/script/editor.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | const runtimes = []; 3 | 4 | function assemble(runtime) { 5 | runtimes.push(runtime); 6 | } 7 | 8 | function KMEditor(selector, editMenuProps) { 9 | this.selector = selector; 10 | for (const runtime of runtimes) { 11 | if (typeof runtime == 'function' && isEnable(editMenuProps, runtime)) { 12 | runtime.call(this, this); 13 | } 14 | } 15 | } 16 | 17 | function isEnable(editMenuProps, runtime) { 18 | switch (runtime.name) { 19 | case "PriorityRuntime": 20 | return !!editMenuProps.sequenceEnable; 21 | case "TagRuntime": 22 | return !!editMenuProps.tagEnable; 23 | case "ProgressRuntime": 24 | return !!editMenuProps.progressEnable; 25 | default: 26 | return true 27 | } 28 | } 29 | 30 | KMEditor.assemble = assemble; 31 | 32 | assemble(require('./runtime/container')); 33 | assemble(require('./runtime/fsm')); 34 | assemble(require('./runtime/minder')); 35 | assemble(require('./runtime/receiver')); 36 | assemble(require('./runtime/hotbox')); 37 | assemble(require('./runtime/input')); 38 | assemble(require('./runtime/clipboard-mimetype')); 39 | assemble(require('./runtime/clipboard')); 40 | assemble(require('./runtime/drag')); 41 | assemble(require('./runtime/node')); 42 | assemble(require('./runtime/history')); 43 | assemble(require('./runtime/jumping')); 44 | assemble(require('./runtime/priority')); 45 | assemble(require('./runtime/progress')); 46 | assemble(require('./runtime/exports')); 47 | assemble(require('./runtime/tag')); 48 | 49 | return module.exports = KMEditor; 50 | }); 51 | -------------------------------------------------------------------------------- /src/script/expose-editor.js: -------------------------------------------------------------------------------- 1 | define('expose-editor', function(require, exports, module) { 2 | return module.exports = kityminder.Editor = require('./editor'); 3 | }); 4 | -------------------------------------------------------------------------------- /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 | return module.exports = window.kityminder.Minder; 3 | }); 4 | -------------------------------------------------------------------------------- /src/script/protocol/freemind.js: -------------------------------------------------------------------------------- 1 | const priorities = [ 2 | {jp: 1, mp: 'full-1'}, 3 | {jp: 2, mp: 'full-2'}, 4 | {jp: 3, mp: 'full-3'}, 5 | {jp: 4, mp: 'full-4'}, 6 | {jp: 5, mp: 'full-5'}, 7 | {jp: 6, mp: 'full-6'}, 8 | {jp: 7, mp: 'full-7'}, 9 | {jp: 8, mp: 'full-8'} 10 | ]; 11 | const mmVersion = '\n'; 12 | const iconTextPrefix = '\n'; 14 | const nodeCreated = '\n'; 18 | const entityNode = '\n'; 19 | const entityMap = ''; 20 | 21 | function exportFreeMind(minder) { 22 | var minds = minder.exportJson(); 23 | var mmContent = mmVersion + traverseJson(minds.root) + entityNode + entityMap; 24 | try { 25 | const link = document.createElement('a'); 26 | const blob = new Blob(["\ufeff" + mmContent], { 27 | type: 'text/xml' 28 | }); 29 | link.href = window.URL.createObjectURL(blob); 30 | link.download = `${minds.root.data.text}.mm`; 31 | document.body.appendChild(link); 32 | link.click(); 33 | document.body.removeChild(link); 34 | } catch (err) { 35 | alert(err); 36 | } 37 | } 38 | 39 | function traverseJson(node){ 40 | var result = ""; 41 | if (!node) { 42 | return; 43 | } 44 | result += concatNodes(node); 45 | if (node.children && node.children.length > 0) { 46 | for (var i = 0; i < node.children.length; i++) { 47 | result += traverseJson(node.children[i]); 48 | result += entityNode; 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | function concatNodes(node) { 55 | var result = ""; 56 | var datas = node.data; 57 | result += nodeCreated + datas.created + nodeId + datas.id + nodeText + datas.text + nodeSuffix; 58 | if (datas.priority) { 59 | var mapped = priorities.find(d => { 60 | return d.jp == datas.priority 61 | }); 62 | if (mapped) { 63 | result += iconTextPrefix + mapped.mp + iconTextSuffix; 64 | } 65 | } 66 | return result; 67 | } 68 | 69 | export { 70 | exportFreeMind 71 | } 72 | -------------------------------------------------------------------------------- /src/script/protocol/json.js: -------------------------------------------------------------------------------- 1 | function exportJson(minder) { 2 | var minds = minder.exportJson(); 3 | try { 4 | const link = document.createElement('a'); 5 | const blob = new Blob(["\ufeff" + JSON.stringify(minds)], { 6 | type: 'text/json' 7 | }); 8 | link.href = window.URL.createObjectURL(blob); 9 | link.download = `${minds.root.data.text}.json`; 10 | document.body.appendChild(link); 11 | link.click(); 12 | document.body.removeChild(link); 13 | } catch (err) { 14 | alert(err); 15 | } 16 | } 17 | 18 | export { 19 | exportJson 20 | } 21 | -------------------------------------------------------------------------------- /src/script/protocol/markdown.js: -------------------------------------------------------------------------------- 1 | const LINE_ENDING_SPLITER = /\r\n|\r|\n/; 2 | const EMPTY_LINE = ''; 3 | const NOTE_MARK_START = ''; 4 | const NOTE_MARK_CLOSE = ''; 5 | 6 | function exportMarkdown(minder) { 7 | var minds = minder.exportJson(); 8 | try { 9 | const link = document.createElement('a'); 10 | const blob = new Blob(["\ufeff" + encode(minds.root, 0)], { 11 | type: 'markdown' 12 | }); 13 | link.href = window.URL.createObjectURL(blob); 14 | link.download = `${minds.root.data.text}.md`; 15 | document.body.appendChild(link); 16 | link.click(); 17 | document.body.removeChild(link); 18 | } catch (err) { 19 | alert(err); 20 | } 21 | } 22 | 23 | function encode(json) { 24 | return _build(json, 1).join('\n'); 25 | } 26 | 27 | function _build(node, level) { 28 | var lines = []; 29 | 30 | level = level || 1; 31 | 32 | var sharps = _generateHeaderSharp(level); 33 | lines.push(sharps + ' ' + node.data.text); 34 | lines.push(EMPTY_LINE); 35 | 36 | var note = node.data.note; 37 | if (note) { 38 | var hasSharp = /^#/.test(note); 39 | if (hasSharp) { 40 | lines.push(NOTE_MARK_START); 41 | note = note.replace(/^#+/gm, function ($0) { 42 | return sharps + $0; 43 | }); 44 | } 45 | lines.push(note); 46 | if (hasSharp) { 47 | lines.push(NOTE_MARK_CLOSE); 48 | } 49 | lines.push(EMPTY_LINE); 50 | } 51 | 52 | if (node.children) node.children.forEach(function (child) { 53 | lines = lines.concat(_build(child, level + 1)); 54 | }); 55 | 56 | return lines; 57 | } 58 | 59 | function _generateHeaderSharp(level) { 60 | var sharps = ''; 61 | while (level--) sharps += '#'; 62 | return sharps; 63 | } 64 | 65 | function decode(markdown) { 66 | 67 | var json, 68 | parentMap = {}, 69 | lines, line, lineInfo, level, node, parent, noteProgress, codeBlock; 70 | 71 | // 一级标题转换 `{title}\n===` => `# {title}` 72 | markdown = markdown.replace(/^(.+)\n={3,}/, function ($0, $1) { 73 | return '# ' + $1; 74 | }); 75 | 76 | lines = markdown.split(LINE_ENDING_SPLITER); 77 | 78 | // 按行分析 79 | for (var i = 0; i < lines.length; i++) { 80 | line = lines[i]; 81 | 82 | lineInfo = _resolveLine(line); 83 | 84 | // 备注标记处理 85 | if (lineInfo.noteClose) { 86 | noteProgress = false; 87 | continue; 88 | } else if (lineInfo.noteStart) { 89 | noteProgress = true; 90 | continue; 91 | } 92 | 93 | // 代码块处理 94 | codeBlock = lineInfo.codeBlock ? !codeBlock : codeBlock; 95 | 96 | // 备注条件:备注标签中,非标题定义,或标题越位 97 | if (noteProgress || codeBlock || !lineInfo.level || lineInfo.level > level + 1) { 98 | if (node) _pushNote(node, line); 99 | continue; 100 | } 101 | 102 | // 标题处理 103 | level = lineInfo.level; 104 | node = _initNode(lineInfo.content, parentMap[level - 1]); 105 | parentMap[level] = node; 106 | } 107 | 108 | _cleanUp(parentMap[1]); 109 | return parentMap[1]; 110 | } 111 | 112 | function _initNode(text, parent) { 113 | var node = { 114 | data: { 115 | text: text, 116 | note: '' 117 | } 118 | }; 119 | if (parent) { 120 | if (parent.children) parent.children.push(node); 121 | else parent.children = [node]; 122 | } 123 | return node; 124 | } 125 | 126 | function _pushNote(node, line) { 127 | node.data.note += line + '\n'; 128 | } 129 | 130 | function _isEmpty(line) { 131 | return !/\S/.test(line); 132 | } 133 | 134 | function _resolveLine(line) { 135 | var match = /^(#+)?\s*(.*)$/.exec(line); 136 | return { 137 | level: match[1] && match[1].length || null, 138 | content: match[2], 139 | noteStart: line == NOTE_MARK_START, 140 | noteClose: line == NOTE_MARK_CLOSE, 141 | codeBlock: /^\s*```/.test(line) 142 | }; 143 | } 144 | 145 | function _cleanUp(node) { 146 | if (!/\S/.test(node.data.note)) { 147 | node.data.note = null; 148 | delete node.data.note; 149 | } else { 150 | var notes = node.data.note.split('\n'); 151 | while (notes.length && !/\S/.test(notes[0])) notes.shift(); 152 | while (notes.length && !/\S/.test(notes[notes.length - 1])) notes.pop(); 153 | node.data.note = notes.join('\n'); 154 | } 155 | if (node.children) node.children.forEach(_cleanUp); 156 | } 157 | 158 | export { 159 | exportMarkdown 160 | } 161 | -------------------------------------------------------------------------------- /src/script/protocol/plain.js: -------------------------------------------------------------------------------- 1 | const LINE_ENDING = '\r'; 2 | const LINE_ENDING_SPLITER = /\r\n|\r|\n/; 3 | const TAB_CHAR = '\t'; 4 | 5 | function exportTextTree(minder) { 6 | var minds = minder.exportJson(); 7 | try { 8 | const link = document.createElement('a'); 9 | const blob = new Blob(["\ufeff" + encode(minds.root, 0)], { 10 | type: 'text/plain' 11 | }); 12 | link.href = window.URL.createObjectURL(blob); 13 | link.download = `${minds.root.data.text}.txt`; 14 | document.body.appendChild(link); 15 | link.click(); 16 | document.body.removeChild(link); 17 | } catch (err) { 18 | alert(err); 19 | } 20 | } 21 | 22 | function repeat(s, n) { 23 | var result = ''; 24 | while (n--) result += s; 25 | return result; 26 | } 27 | 28 | function encode(json, level) { 29 | var local = ''; 30 | level = level || 0; 31 | local += repeat(TAB_CHAR, level); 32 | local += json.data.text + LINE_ENDING; 33 | if (json.children) { 34 | json.children.forEach(function (child) { 35 | local += encode(child, level + 1); 36 | }); 37 | } 38 | return local; 39 | } 40 | 41 | function isEmpty(line) { 42 | return !/\S/.test(line); 43 | } 44 | 45 | function getLevel(line) { 46 | var level = 0; 47 | while (line.charAt(level) === TAB_CHAR) level++; 48 | return level; 49 | } 50 | 51 | function getNode(line) { 52 | return { 53 | data: { 54 | text: line.replace(new RegExp('^' + TAB_CHAR + '*'), '') 55 | } 56 | }; 57 | } 58 | 59 | /** 60 | * 文本解码 61 | * 62 | * @param {string} local 文本内容 63 | * @param {=boolean} root 自动根节点 64 | * @return {Object} 返回解析后节点 65 | */ 66 | function decode(local, root) { 67 | var json, 68 | offset, 69 | parentMap = {}, 70 | lines = local.split(LINE_ENDING_SPLITER), 71 | line, level, node; 72 | 73 | function addChild(parent, child) { 74 | var children = parent.children || (parent.children = []); 75 | children.push(child); 76 | } 77 | if (root) { 78 | parentMap[0] = json = getNode('root'); 79 | offset = 1; 80 | } else { 81 | offset = 0; 82 | } 83 | 84 | for (var i = 0; i < lines.length; i++) { 85 | line = lines[i]; 86 | if (isEmpty(line)) continue; 87 | 88 | level = getLevel(line) + offset; 89 | node = getNode(line); 90 | 91 | if (level === 0) { 92 | if (json) { 93 | throw new Error('Invalid local format'); 94 | } 95 | json = node; 96 | } else { 97 | if (!parentMap[level - 1]) { 98 | throw new Error('Invalid local format'); 99 | } 100 | addChild(parentMap[level - 1], node); 101 | } 102 | parentMap[level] = node; 103 | } 104 | return json; 105 | } 106 | 107 | export { 108 | exportTextTree 109 | } 110 | -------------------------------------------------------------------------------- /src/script/protocol/png.js: -------------------------------------------------------------------------------- 1 | var DOMURL = window.URL || window.webkitURL || window; 2 | 3 | function downloadImage(fileURI, fileName) { 4 | try { 5 | const link = document.createElement('a'); 6 | link.href = fileURI; 7 | link.download = `${fileName}.png`; 8 | document.body.appendChild(link); 9 | link.click(); 10 | document.body.removeChild(link); 11 | } catch (err) { 12 | alert(err); 13 | } 14 | } 15 | 16 | function loadImage(url, callback) { 17 | return new Promise(function (resolve, reject) { 18 | var image = document.createElement('img'); 19 | image.onload = function () { 20 | resolve(this); 21 | }; 22 | image.onerror = function (err) { 23 | reject(err); 24 | }; 25 | image.crossOrigin = ''; 26 | image.src = url; 27 | }); 28 | } 29 | 30 | function getSVGInfo(minder) { 31 | var paper = minder.getPaper(), 32 | paperTransform, 33 | domContainer = paper.container, 34 | svgXml, 35 | $svg, 36 | 37 | renderContainer = minder.getRenderContainer(), 38 | renderBox = renderContainer.getRenderBox(), 39 | width = renderBox.width + 1, 40 | height = renderBox.height + 1, 41 | 42 | blob, svgUrl, img; 43 | 44 | // 保存原始变换,并且移动到合适的位置 45 | paperTransform = paper.shapeNode.getAttribute('transform'); 46 | paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)'); 47 | renderContainer.translate(-renderBox.x, -renderBox.y); 48 | 49 | // 获取当前的 XML 代码 50 | svgXml = paper.container.innerHTML; 51 | 52 | // 回复原始变换及位置 53 | renderContainer.translate(renderBox.x, renderBox.y); 54 | paper.shapeNode.setAttribute('transform', paperTransform); 55 | 56 | // 过滤内容 57 | let el = document.createElement("div"); 58 | el.innerHTML = svgXml; 59 | $svg = el.getElementsByTagName('svg'); 60 | 61 | let index = $svg.length - 1; 62 | 63 | $svg[index].setAttribute('width', renderBox.width + 1); 64 | $svg[index].setAttribute('height', renderBox.height + 1); 65 | $svg[index].setAttribute('style', 'font-family: Arial, "Microsoft Yahei","Heiti SC";'); 66 | 67 | let div = document.createElement("div"); 68 | div.appendChild($svg[index]); 69 | svgXml = div.innerHTML; 70 | 71 | // Dummy IE 72 | svgXml = svgXml.replace(' xmlns="http://www.w3.org/2000/svg" xmlns:NS1="" NS1:ns1:xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:NS2="" NS2:xmlns:ns1=""', ''); 73 | 74 | // svg 含有   符号导出报错 Entity 'nbsp' not defined 75 | svgXml = svgXml.replace(/ /g, ' '); 76 | 77 | blob = new Blob([svgXml], { 78 | type: 'image/svg+xml' 79 | }); 80 | 81 | svgUrl = DOMURL.createObjectURL(blob); 82 | 83 | return { 84 | width: width, 85 | height: height, 86 | dataUrl: svgUrl, 87 | xml: svgXml 88 | }; 89 | } 90 | 91 | function exportPNGImage(minder) { 92 | 93 | /* 绘制 PNG 的画布及上下文 */ 94 | var canvas = document.createElement('canvas'); 95 | var ctx = canvas.getContext('2d'); 96 | 97 | /* 尝试获取背景图片 URL 或背景颜色 */ 98 | var bgDeclare = minder.getStyle('background').toString(); 99 | var bgUrl = /url\((.+)\)/.exec(bgDeclare); 100 | var bgColor = kity.Color.parse(bgDeclare); 101 | 102 | /* 获取 SVG 文件内容 */ 103 | var svgInfo = getSVGInfo(minder); 104 | var width = svgInfo.width; 105 | var height = svgInfo.height; 106 | var svgDataUrl = svgInfo.dataUrl; 107 | 108 | /* 画布的填充大小 */ 109 | var padding = 20; 110 | 111 | canvas.width = width + padding * 2; 112 | canvas.height = height + padding * 2; 113 | 114 | function fillBackground(ctx, style) { 115 | ctx.save(); 116 | ctx.fillStyle = style; 117 | ctx.fillRect(0, 0, canvas.width, canvas.height); 118 | ctx.restore(); 119 | } 120 | 121 | function drawImage(ctx, image, x, y) { 122 | ctx.drawImage(image, x, y); 123 | } 124 | 125 | function generateDataUrl(canvas) { 126 | try { 127 | var url = canvas.toDataURL('png'); 128 | return url; 129 | } catch (e) { 130 | throw new Error('当前浏览器版本不支持导出 PNG 功能,请尝试升级到最新版本!'); 131 | } 132 | } 133 | 134 | function drawSVG(minder) { 135 | var mind = editor.minder.exportJson(); 136 | if (typeof (window.canvg) != 'undefined') { 137 | return new Promise(function (resolve) { 138 | window.canvg(canvas, svgInfo.xml, { 139 | ignoreMouse: true, 140 | ignoreAnimation: true, 141 | ignoreDimensions: true, 142 | ignoreClear: true, 143 | offsetX: padding, 144 | offsetY: padding, 145 | renderCallback: function () { 146 | downloadImage(generateDataUrl(canvas), mind.root.data.text); 147 | } 148 | }); 149 | }); 150 | } else { 151 | return loadImage(svgDataUrl).then(function (svgImage) { 152 | drawImage(ctx, svgImage, padding, padding); 153 | DOMURL.revokeObjectURL(svgDataUrl); 154 | downloadImage(generateDataUrl(canvas), mind.root.data.text); 155 | }); 156 | } 157 | } 158 | 159 | if (bgUrl) { 160 | loadImage(bgUrl[1]).then(function (image) { 161 | fillBackground(ctx, ctx.createPattern(image, 'repeat')); 162 | drawSVG(minder); 163 | }); 164 | } else { 165 | fillBackground(ctx, bgColor.toString()); 166 | drawSVG(minder); 167 | } 168 | } 169 | 170 | export { 171 | exportPNGImage 172 | } 173 | -------------------------------------------------------------------------------- /src/script/protocol/svg.js: -------------------------------------------------------------------------------- 1 | function exportSVG(minder) { 2 | 3 | var paper = minder.getPaper(); 4 | var paperTransform = paper.shapeNode.getAttribute('transform'); 5 | var svgXml; 6 | var $svg; 7 | 8 | var renderContainer = minder.getRenderContainer(); 9 | var renderBox = renderContainer.getRenderBox(); 10 | var transform = renderContainer.getTransform(); 11 | var width = renderBox.width; 12 | var height = renderBox.height; 13 | var padding = 20; 14 | 15 | paper.shapeNode.setAttribute('transform', 'translate(0.5, 0.5)'); 16 | svgXml = paper.container.innerHTML; 17 | console.log(svgXml); 18 | paper.shapeNode.setAttribute('transform', paperTransform); 19 | 20 | let document = window.document; 21 | let el = document.createElement("div"); 22 | el.innerHTML = svgXml; 23 | $svg = el.getElementsByTagName('svg'); 24 | 25 | let index = $svg.length - 1; 26 | 27 | $svg[index].setAttribute('width', width + padding * 2 | 0); 28 | $svg[index].setAttribute('height', height + padding * 2 | 0); 29 | $svg[index].setAttribute('style', 'font-family: Arial, "Microsoft Yahei", "Heiti SC"; background: ' + minder.getStyle('background')); 30 | 31 | $svg[index].setAttribute('viewBox', [renderBox.x - padding | 0, 32 | renderBox.y - padding | 0, 33 | width + padding * 2 | 0, 34 | height + padding * 2 | 0 35 | ].join(' ')); 36 | 37 | let div = document.createElement("div"); 38 | div.appendChild($svg[index]); 39 | svgXml = div.innerHTML; 40 | svgXml = svgXml.replace(/ /g, ' '); 41 | 42 | var blob = new Blob([svgXml], { 43 | type: 'image/svg+xml' 44 | }); 45 | 46 | var DOMURL = window.URL || window.webkitURL || window; 47 | var svgUrl = DOMURL.createObjectURL(blob); 48 | 49 | var mind = editor.minder.exportJson(); 50 | downloadSVG(svgUrl, mind.root.data.text); 51 | } 52 | 53 | function downloadSVG(fileURI, fileName) { 54 | try { 55 | const link = document.createElement('a'); 56 | link.href = fileURI; 57 | link.download = `${fileName}.svg`; 58 | document.body.appendChild(link); 59 | link.click(); 60 | document.body.removeChild(link); 61 | } catch (err) { 62 | alert(err); 63 | } 64 | } 65 | 66 | export { 67 | exportSVG 68 | } 69 | -------------------------------------------------------------------------------- /src/script/protocol/xmind.js: -------------------------------------------------------------------------------- 1 | const priorities = [ 2 | {jp: 1, mp: 'full-1'}, 3 | {jp: 2, mp: 'full-2'}, 4 | {jp: 3, mp: 'full-3'}, 5 | {jp: 4, mp: 'full-4'}, 6 | {jp: 5, mp: 'full-5'}, 7 | {jp: 6, mp: 'full-6'}, 8 | {jp: 7, mp: 'full-7'}, 9 | {jp: 8, mp: 'full-8'} 10 | ]; 11 | const mmVersion = '\n'; 12 | const iconTextPrefix = '\n'; 14 | const nodeCreated = '\n'; 18 | const entityNode = '\n'; 19 | const entityMap = ''; 20 | 21 | function exportXMind(minder) { 22 | var minds = minder.exportJson(); 23 | var mmContent = mmVersion + traverseJson(minds.root) + entityNode + entityMap; 24 | try { 25 | const link = document.createElement('a'); 26 | const blob = new Blob(["\ufeff" + mmContent], { 27 | type: 'text/xml' 28 | }); 29 | link.href = window.URL.createObjectURL(blob); 30 | link.download = `${minds.root.data.text}.mm`; 31 | document.body.appendChild(link); 32 | link.click(); 33 | document.body.removeChild(link); 34 | } catch (err) { 35 | alert(err); 36 | } 37 | } 38 | 39 | function traverseJson(node){ 40 | var result = ""; 41 | if (!node) { 42 | return; 43 | } 44 | result += concatNodes(node); 45 | if (node.children && node.children.length > 0) { 46 | for (var i = 0; i < node.children.length; i++) { 47 | result += traverseJson(node.children[i]); 48 | result += entityNode; 49 | } 50 | } 51 | return result; 52 | } 53 | 54 | function concatNodes(node) { 55 | var result = ""; 56 | var datas = node.data; 57 | result += nodeCreated + datas.created + nodeId + datas.id + nodeText + datas.text + nodeSuffix; 58 | if (datas.priority) { 59 | var mapped = priorities.find(d => { 60 | return d.jp == datas.priority 61 | }); 62 | if (mapped) { 63 | result += iconTextPrefix + mapped.mp + iconTextSuffix; 64 | } 65 | } 66 | return result; 67 | } 68 | 69 | export { 70 | exportXMind 71 | } 72 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard-mimetype.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | function MimeType() { 3 | var SPLITOR = '\uFEFF'; 4 | var MIMETYPE = { 5 | 'application/km': '\uFFFF' 6 | }; 7 | var SIGN = { 8 | '\uFEFF': 'SPLITOR', 9 | '\uFFFF': 'application/km' 10 | }; 11 | 12 | function process(mimetype, text) { 13 | if (!this.isPureText(text)) { 14 | var _mimetype = this.whichMimeType(text); 15 | if (!_mimetype) { 16 | throw new Error('unknow mimetype!'); 17 | }; 18 | text = this.getPureText(text); 19 | }; 20 | if (mimetype === false) { 21 | return text; 22 | }; 23 | return mimetype + SPLITOR + text; 24 | } 25 | 26 | this.registMimeTypeProtocol = function (type, sign) { 27 | if (sign && SIGN[sign]) { 28 | throw new Error('sing has registed!'); 29 | } 30 | if (type && !!MIMETYPE[type]) { 31 | throw new Error('mimetype has registed!'); 32 | }; 33 | SIGN[sign] = type; 34 | MIMETYPE[type] = sign; 35 | } 36 | 37 | this.getMimeTypeProtocol = function (type, text) { 38 | var mimetype = MIMETYPE[type] || false; 39 | 40 | if (text === undefined) { 41 | return process.bind(this, mimetype); 42 | }; 43 | 44 | return process(mimetype, text); 45 | } 46 | 47 | this.getSpitor = function () { 48 | return SPLITOR; 49 | } 50 | 51 | this.getMimeType = function (sign) { 52 | if (sign !== undefined) { 53 | return SIGN[sign] || null; 54 | }; 55 | return MIMETYPE; 56 | } 57 | } 58 | 59 | MimeType.prototype.isPureText = function (text) { 60 | return !(~text.indexOf(this.getSpitor())); 61 | } 62 | 63 | MimeType.prototype.getPureText = function (text) { 64 | if (this.isPureText(text)) { 65 | return text; 66 | }; 67 | return text.split(this.getSpitor())[1]; 68 | } 69 | 70 | MimeType.prototype.whichMimeType = function (text) { 71 | if (this.isPureText(text)) { 72 | return null; 73 | }; 74 | return this.getMimeType(text.split(this.getSpitor())[0]); 75 | } 76 | 77 | function MimeTypeRuntime() { 78 | if (this.minder.supportClipboardEvent && !kity.Browser.gecko) { 79 | this.MimeType = new MimeType(); 80 | }; 81 | } 82 | 83 | return module.exports = MimeTypeRuntime; 84 | }); 85 | -------------------------------------------------------------------------------- /src/script/runtime/clipboard.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | function ClipboardRuntime() { 4 | var minder = this.minder; 5 | var Data = window.kityminder.data; 6 | 7 | var {markDeleteNode, resetNodes} = require('../tool/utils'); 8 | 9 | 10 | if (!minder.supportClipboardEvent || kity.Browser.gecko) { 11 | return; 12 | }; 13 | 14 | var fsm = this.fsm; 15 | var receiver = this.receiver; 16 | var MimeType = this.MimeType; 17 | 18 | var kmencode = MimeType.getMimeTypeProtocol('application/km'), 19 | decode = Data.getRegisterProtocol('json').decode; 20 | var _selectedNodes = []; 21 | 22 | /* 23 | * 增加对多节点赋值粘贴的处理 24 | */ 25 | function encode(nodes) { 26 | var _nodes = []; 27 | for (var i = 0, l = nodes.length; i < l; i++) { 28 | _nodes.push(minder.exportNode(nodes[i])); 29 | } 30 | return kmencode(Data.getRegisterProtocol('json').encode(_nodes)); 31 | } 32 | 33 | var beforeCopy = function (e) { 34 | if (document.activeElement == receiver.element) { 35 | var clipBoardEvent = e; 36 | var state = fsm.state(); 37 | 38 | switch (state) { 39 | case 'input': { 40 | break; 41 | } 42 | case 'normal': { 43 | var nodes = [].concat(minder.getSelectedNodes()); 44 | if (nodes.length) { 45 | // 这里由于被粘贴复制的节点的id信息也都一样,故做此算法 46 | // 这里有个疑问,使用node.getParent()或者node.parent会离奇导致出现非选中节点被渲染成选中节点,因此使用isAncestorOf,而没有使用自行回溯的方式 47 | if (nodes.length > 1) { 48 | var targetLevel; 49 | nodes.sort(function (a, b) { 50 | return a.getLevel() - b.getLevel(); 51 | }); 52 | targetLevel = nodes[0].getLevel(); 53 | if (targetLevel !== nodes[nodes.length - 1].getLevel()) { 54 | var plevel, pnode, 55 | idx = 0, 56 | l = nodes.length, 57 | 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 | markDeleteNode(minder); 101 | var nodes = minder.getSelectedNodes(); 102 | if (nodes.length) { 103 | clipBoardEvent.clipboardData.setData('text/plain', encode(nodes)); 104 | minder.execCommand('removenode'); 105 | } 106 | e.preventDefault(); 107 | break; 108 | } 109 | } 110 | }; 111 | } 112 | 113 | var beforePaste = function (e) { 114 | if (document.activeElement == receiver.element) { 115 | if (minder.getStatus() !== 'normal') { 116 | e.preventDefault(); 117 | return; 118 | }; 119 | 120 | var clipBoardEvent = e; 121 | var state = fsm.state(); 122 | var textData = clipBoardEvent.clipboardData.getData('text/plain'); 123 | 124 | switch (state) { 125 | case 'input': { 126 | // input状态下如果格式为application/km则不进行paste操作 127 | if (!MimeType.isPureText(textData)) { 128 | e.preventDefault(); 129 | return; 130 | }; 131 | break; 132 | } 133 | case 'normal': { 134 | /* 135 | * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理 136 | */ 137 | var sNodes = minder.getSelectedNodes(); 138 | 139 | if (MimeType.whichMimeType(textData) === 'application/km') { 140 | var nodes = decode(MimeType.getPureText(textData)); 141 | resetNodes(nodes); 142 | var _node; 143 | sNodes.forEach(function (node) { 144 | // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来 145 | for (var i = nodes.length - 1; i >= 0; i--) { 146 | _node = minder.createNode(null, node); 147 | minder.importNode(_node, nodes[i]); 148 | _selectedNodes.push(_node); 149 | node.appendChild(_node); 150 | } 151 | }); 152 | minder.select(_selectedNodes, true); 153 | _selectedNodes = []; 154 | 155 | minder.refresh(); 156 | } else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) { 157 | var imageFile = clipBoardEvent.clipboardData.items[0].getAsFile(); 158 | var serverService = angular.element(document.body).injector().get('server'); 159 | 160 | return serverService.uploadImage(imageFile).then(function (json) { 161 | var resp = json.data; 162 | if (resp.errno === 0) { 163 | minder.execCommand('image', resp.data.url); 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 | minder.execCommand('paste'); 177 | } 178 | } 179 | 180 | /** 181 | * 由editor的receiver统一处理全部事件,包括clipboard事件 182 | * @Editor: Naixor 183 | * @Date: 2015.9.24 184 | */ 185 | document.addEventListener('copy', beforeCopy); 186 | document.addEventListener('cut', beforeCut); 187 | document.addEventListener('paste', beforePaste); 188 | } 189 | 190 | return module.exports = ClipboardRuntime; 191 | }); 192 | -------------------------------------------------------------------------------- /src/script/runtime/container.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | function ContainerRuntime() { 3 | var container; 4 | 5 | if (typeof (this.selector) == 'string') { 6 | container = document.querySelector(this.selector); 7 | } else { 8 | container = this.selector; 9 | } 10 | 11 | if (!container) throw new Error('Invalid selector: ' + this.selector); 12 | 13 | // 这个类名用于给编辑器添加样式 14 | container.classList.add('km-editor'); 15 | 16 | // 暴露容器给其他运行时使用 17 | this.container = container; 18 | } 19 | 20 | return module.exports = ContainerRuntime; 21 | }); 22 | -------------------------------------------------------------------------------- /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, 47 | freeVirtical = false; 48 | var frame; 49 | 50 | function move(direction, speed) { 51 | if (!direction) { 52 | freeHorizen = freeVirtical = false; 53 | frame && kity.releaseFrame(frame); 54 | frame = null; 55 | return; 56 | } 57 | if (!frame) { 58 | frame = kity.requestFrame((function (direction, speed, minder) { 59 | return function (frame) { 60 | switch (direction) { 61 | case 'left': 62 | minder._viewDragger.move({ 63 | x: -speed, 64 | y: 0 65 | }, 0); 66 | break; 67 | case 'top': 68 | minder._viewDragger.move({ 69 | x: 0, 70 | y: -speed 71 | }, 0); 72 | break; 73 | case 'right': 74 | minder._viewDragger.move({ 75 | x: speed, 76 | y: 0 77 | }, 0); 78 | break; 79 | case 'bottom': 80 | minder._viewDragger.move({ 81 | x: 0, 82 | y: speed 83 | }, 0); 84 | break; 85 | default: 86 | return; 87 | } 88 | frame.next(); 89 | }; 90 | })(direction, speed, minder)); 91 | } 92 | } 93 | 94 | minder.on('mousedown', function (e) { 95 | flag = MOUSE_HAS_DOWN; 96 | var rect = minder.getPaper().container.getBoundingClientRect(); 97 | downX = e.originEvent.clientX; 98 | downY = e.originEvent.clientY; 99 | containerY = rect.top; 100 | maxX = rect.width; 101 | maxY = rect.height; 102 | }); 103 | 104 | minder.on('mousemove', function (e) { 105 | if (fsm.state() === 'drag' && flag == MOUSE_HAS_DOWN && minder.getSelectedNode() && 106 | (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || 107 | Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) { 108 | osx = e.originEvent.clientX; 109 | osy = e.originEvent.clientY - containerY; 110 | 111 | if (osx < BOUND_CHECK) { 112 | move('right', BOUND_CHECK - osx); 113 | } else if (osx > maxX - BOUND_CHECK) { 114 | move('left', BOUND_CHECK + osx - maxX); 115 | } else { 116 | freeHorizen = true; 117 | } 118 | if (osy < BOUND_CHECK) { 119 | move('bottom', osy); 120 | } else if (osy > maxY - BOUND_CHECK) { 121 | move('top', BOUND_CHECK + osy - maxY); 122 | } else { 123 | freeVirtical = true; 124 | } 125 | if (freeHorizen && freeVirtical) { 126 | move(false); 127 | } 128 | } 129 | if (fsm.state() !== 'drag' && 130 | flag === MOUSE_HAS_DOWN && 131 | minder.getSelectedNode() && 132 | (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK || 133 | Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) { 134 | 135 | if (fsm.state() === 'hotbox') { 136 | hotbox.active(Hotbox.STATE_IDLE); 137 | } 138 | 139 | return fsm.jump('drag', 'user-drag'); 140 | } 141 | }); 142 | 143 | window.addEventListener('mouseup', function () { 144 | flag = MOUSE_HAS_UP; 145 | if (fsm.state() === 'drag') { 146 | move(false); 147 | return fsm.jump('normal', 'drag-finish'); 148 | } 149 | }, false); 150 | } 151 | 152 | return module.exports = DragRuntime; 153 | }); 154 | -------------------------------------------------------------------------------- /src/script/runtime/exports.js: -------------------------------------------------------------------------------- 1 | 2 | define(function (require, exports, module) { 3 | var png = require("../protocol/png"); 4 | var svg = require("../protocol/svg"); 5 | var json = require("../protocol/json"); 6 | var plain = require("../protocol/plain"); 7 | var md = require("../protocol/markdown"); 8 | var mm = require("../protocol/freemind"); 9 | var {t} = require("../../locale"); 10 | 11 | function ExportRuntime() { 12 | var minder = this.minder; 13 | var hotbox = this.hotbox; 14 | var exps = [ 15 | {label: '.json', key: 'j', cmd: exportJson}, 16 | {label: '.png', key: 'p', cmd: exportImage}, 17 | {label: '.svg', key: 's', cmd: exportSVG}, 18 | {label: '.txt', key: 't', cmd: exportTextTree}, 19 | {label: '.md', key: 'm', cmd: exportMarkdown}, 20 | {label: '.mm', key: 'f', cmd: exportFreeMind} 21 | ]; 22 | 23 | 24 | var main = hotbox.state('main'); 25 | main.button({ 26 | position: 'top', 27 | label: t('minder.commons.export'), 28 | key: 'E', 29 | enable: canExp, 30 | next: 'exp' 31 | }); 32 | 33 | var exp = hotbox.state('exp'); 34 | exps.forEach(item => { 35 | exp.button({ 36 | position: 'ring', 37 | label: item.label, 38 | key: null, 39 | action: item.cmd 40 | }); 41 | }); 42 | 43 | exp.button({ 44 | position: 'center', 45 | label: t('minder.commons.cancel'), 46 | key: 'esc', 47 | next: 'back' 48 | }); 49 | 50 | function canExp() { 51 | return true; 52 | } 53 | 54 | function exportJson(){ 55 | json.exportJson(minder); 56 | } 57 | 58 | function exportImage (){ 59 | png.exportPNGImage(minder); 60 | } 61 | 62 | function exportSVG (){ 63 | svg.exportSVG(minder); 64 | } 65 | 66 | function exportTextTree (){ 67 | plain.exportTextTree(minder); 68 | } 69 | 70 | function exportMarkdown (){ 71 | md.exportMarkdown(minder); 72 | } 73 | 74 | function exportFreeMind (){ 75 | mm.exportFreeMind(minder); 76 | } 77 | } 78 | 79 | return module.exports = ExportRuntime; 80 | }); 81 | -------------------------------------------------------------------------------- /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 | }); 122 | -------------------------------------------------------------------------------- /src/script/runtime/history.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | function HistoryRuntime() { 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | var {isDisableNode} = require('../tool/utils'); 7 | var {t} = require("../../locale"); 8 | 9 | var MAX_HISTORY = 100; 10 | 11 | var lastSnap; 12 | var patchLock; 13 | var undoDiffs; 14 | var redoDiffs; 15 | 16 | function reset() { 17 | undoDiffs = []; 18 | redoDiffs = []; 19 | lastSnap = minder.exportJson(); 20 | } 21 | 22 | var _objectKeys = (function () { 23 | if (Object.keys) 24 | return Object.keys; 25 | 26 | return function (o) { 27 | var keys = []; 28 | for (var i in o) { 29 | if (o.hasOwnProperty(i)) { 30 | keys.push(i); 31 | } 32 | } 33 | return keys; 34 | }; 35 | })(); 36 | 37 | function escapePathComponent(str) { 38 | if (str.indexOf('/') === -1 && str.indexOf('~') === -1) 39 | return str; 40 | return str.replace(/~/g, '~0').replace(/\//g, '~1'); 41 | } 42 | 43 | function deepClone(obj) { 44 | if (typeof obj === "object") { 45 | return JSON.parse(JSON.stringify(obj)); 46 | } else { 47 | return obj; 48 | } 49 | } 50 | 51 | function _generate(mirror, obj, patches, path) { 52 | var newKeys = _objectKeys(obj); 53 | var oldKeys = _objectKeys(mirror); 54 | var changed = false; 55 | var deleted = false; 56 | 57 | for (var t = oldKeys.length - 1; t >= 0; t--) { 58 | var key = oldKeys[t]; 59 | var oldVal = mirror[key]; 60 | if (obj.hasOwnProperty(key)) { 61 | var newVal = obj[key]; 62 | if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { 63 | _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); 64 | } else { 65 | if (oldVal != newVal) { 66 | changed = true; 67 | patches.push({ 68 | op: "replace", 69 | path: path + "/" + escapePathComponent(key), 70 | value: deepClone(newVal) 71 | }); 72 | } 73 | } 74 | } else { 75 | patches.push({ 76 | op: "remove", 77 | path: path + "/" + escapePathComponent(key) 78 | }); 79 | deleted = true; // property has been deleted 80 | } 81 | } 82 | 83 | if (!deleted && newKeys.length == oldKeys.length) { 84 | return; 85 | } 86 | 87 | for (var t = 0; t < newKeys.length; t++) { 88 | var key = newKeys[t]; 89 | if (!mirror.hasOwnProperty(key)) { 90 | patches.push({ 91 | op: "add", 92 | path: path + "/" + escapePathComponent(key), 93 | value: deepClone(obj[key]) 94 | }); 95 | } 96 | } 97 | } 98 | 99 | function jsonDiff(tree1, tree2) { 100 | var patches = []; 101 | _generate(tree1, tree2, patches, ''); 102 | return patches; 103 | } 104 | 105 | function makeUndoDiff() { 106 | var headSnap = minder.exportJson(); 107 | var diff = jsonDiff(headSnap, lastSnap); 108 | if (diff.length) { 109 | undoDiffs.push(diff); 110 | while (undoDiffs.length > MAX_HISTORY) { 111 | undoDiffs.shift(); 112 | } 113 | lastSnap = headSnap; 114 | return true; 115 | } 116 | } 117 | 118 | function makeRedoDiff() { 119 | var revertSnap = minder.exportJson(); 120 | redoDiffs.push(jsonDiff(revertSnap, lastSnap)); 121 | lastSnap = revertSnap; 122 | } 123 | 124 | function undo() { 125 | patchLock = true; 126 | var undoDiff = undoDiffs.pop(); 127 | if (undoDiff) { 128 | minder.applyPatches(undoDiff); 129 | makeRedoDiff(); 130 | } 131 | patchLock = false; 132 | } 133 | 134 | function redo() { 135 | patchLock = true; 136 | var redoDiff = redoDiffs.pop(); 137 | if (redoDiff) { 138 | minder.applyPatches(redoDiff); 139 | makeUndoDiff(); 140 | } 141 | patchLock = false; 142 | } 143 | 144 | function changed() { 145 | if (patchLock) 146 | return; 147 | if (makeUndoDiff()) 148 | redoDiffs = []; 149 | } 150 | 151 | function hasUndo() { 152 | return !!undoDiffs.length; 153 | } 154 | 155 | function hasRedo() { 156 | return !!redoDiffs.length; 157 | } 158 | 159 | function updateSelection(e) { 160 | if (!patchLock) 161 | return; 162 | var patch = e.patch; 163 | switch (patch.express) { 164 | case 'node.add': 165 | minder.select(patch.node.getChild(patch.index), true); 166 | break; 167 | case 'node.remove': 168 | case 'data.replace': 169 | case 'data.remove': 170 | case 'data.add': 171 | minder.select(patch.node, true); 172 | break; 173 | } 174 | } 175 | 176 | this.history = { 177 | reset: reset, 178 | undo: undo, 179 | redo: redo, 180 | hasUndo: hasUndo, 181 | hasRedo: hasRedo 182 | }; 183 | reset(); 184 | minder.on('contentchange', changed); 185 | minder.on('import', reset); 186 | minder.on('patch', updateSelection); 187 | 188 | var main = hotbox.state('main'); 189 | main.button({ 190 | position: 'bottom', 191 | label: t('minder.main.history.undo'), 192 | key: 'Ctrl + Z', 193 | enable: function() { 194 | if (isDisableNode(minder)) { 195 | return false; 196 | } 197 | return hasUndo; 198 | }, 199 | action: undo, 200 | next: 'idle' 201 | }); 202 | main.button({ 203 | position: 'bottom', 204 | label: t('minder.main.history.redo'), 205 | key: 'Ctrl + Y', 206 | enable: function() { 207 | if (isDisableNode(minder)) { 208 | return false; 209 | } 210 | return hasRedo; 211 | }, 212 | action: redo, 213 | next: 'idle' 214 | }); 215 | } 216 | 217 | // window.diff = jsonDiff; 218 | 219 | return module.exports = HistoryRuntime; 220 | }); 221 | -------------------------------------------------------------------------------- /src/script/runtime/hotbox.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var Hotbox = require('../hotbox'); 3 | 4 | function HotboxRuntime() { 5 | var fsm = this.fsm; 6 | var minder = this.minder; 7 | var receiver = this.receiver; 8 | var container = this.container; 9 | 10 | var hotbox = new Hotbox(container); 11 | 12 | hotbox.setParentFSM(fsm); 13 | 14 | fsm.when('normal -> hotbox', function (exit, enter, reason) { 15 | var node = minder.getSelectedNode(); 16 | var position; 17 | if (node) { 18 | var box = node.getRenderBox(); 19 | position = { 20 | x: box.cx, 21 | y: box.cy 22 | }; 23 | } 24 | hotbox.active('main', position); 25 | }); 26 | 27 | fsm.when('normal -> normal', function (exit, enter, reason, e) { 28 | if (reason == 'shortcut-handle') { 29 | var handleResult = hotbox.dispatch(e); 30 | if (handleResult) { 31 | e.preventDefault(); 32 | } else { 33 | minder.dispatchKeyEvent(e); 34 | } 35 | } 36 | }); 37 | 38 | fsm.when('modal -> normal', function (exit, enter, reason, e) { 39 | if (reason == 'import-text-finish') { 40 | receiver.element.focus(); 41 | } 42 | }); 43 | 44 | this.hotbox = hotbox; 45 | minder.hotbox = hotbox; 46 | } 47 | 48 | return module.exports = HotboxRuntime; 49 | }); 50 | -------------------------------------------------------------------------------- /src/script/runtime/jumping.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | 3 | var Hotbox = require('../hotbox'); 4 | 5 | // Nice: http://unixpapa.com/js/key.html 6 | function isIntendToInput(e) { 7 | if (e.ctrlKey || e.metaKey || e.altKey) return false; 8 | 9 | // a-zA-Z 10 | if (e.keyCode >= 65 && e.keyCode <= 90) return true; 11 | 12 | // 0-9 以及其上面的符号 13 | if (e.keyCode >= 48 && e.keyCode <= 57) return true; 14 | 15 | // 小键盘区域 (除回车外) 16 | if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 17 | 18 | // 小键盘区域 (除回车外) 19 | // @yinheli from pull request 20 | if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true; 21 | 22 | // 输入法 23 | if (e.keyCode == 229 || e.keyCode === 0) return true; 24 | 25 | return false; 26 | } 27 | /** 28 | * @Desc: 下方使用receiver.enable()和receiver.disable()通过 29 | * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug; 30 | * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户 31 | * 输入法状态丢失,因此对FF暂不做处理 32 | * @Editor: Naixor 33 | * @Date: 2015.09.14 34 | */ 35 | function JumpingRuntime() { 36 | var fsm = this.fsm; 37 | var minder = this.minder; 38 | var receiver = this.receiver; 39 | var container = this.container; 40 | var receiverElement = receiver.element; 41 | var hotbox = this.hotbox; 42 | 43 | // normal -> * 44 | receiver.listen('normal', function (e) { 45 | // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable 46 | receiver.enable(); 47 | // normal -> hotbox 48 | if (e.is('Space')) { 49 | e.preventDefault(); 50 | // safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉 51 | if (kity.Browser.safari) { 52 | receiverElement.innerHTML = ''; 53 | } 54 | return fsm.jump('hotbox', 'space-trigger'); 55 | } 56 | 57 | /** 58 | * check 59 | * @editor Naixor 60 | * @Date 2015-12-2 61 | */ 62 | switch (e.type) { 63 | case 'keydown': { 64 | if (minder.getSelectedNode()) { 65 | if (isIntendToInput(e)) { 66 | return fsm.jump('input', 'user-input'); 67 | }; 68 | } else { 69 | receiverElement.innerHTML = ''; 70 | } 71 | // normal -> normal shortcut 72 | fsm.jump('normal', 'shortcut-handle', e); 73 | break; 74 | } 75 | case 'keyup': { 76 | break; 77 | } 78 | default: {} 79 | } 80 | }); 81 | 82 | // hotbox -> normal 83 | receiver.listen('hotbox', function (e) { 84 | receiver.disable(); 85 | e.preventDefault(); 86 | var handleResult = hotbox.dispatch(e); 87 | if (hotbox.state() == Hotbox.STATE_IDLE && fsm.state() == 'hotbox') { 88 | return fsm.jump('normal', 'hotbox-idle'); 89 | } 90 | }); 91 | 92 | // input => normal 93 | receiver.listen('input', function (e) { 94 | receiver.enable(); 95 | if (e.type == 'keydown') { 96 | if (e.is('Enter')) { 97 | e.preventDefault(); 98 | return fsm.jump('normal', 'input-commit'); 99 | } 100 | if (e.is('Esc')) { 101 | e.preventDefault(); 102 | return fsm.jump('normal', 'input-cancel'); 103 | } 104 | if (e.is('Tab') || e.is('Shift + Tab')) { 105 | e.preventDefault(); 106 | } 107 | } else if (e.type == 'keyup' && e.is('Esc')) { 108 | e.preventDefault(); 109 | return fsm.jump('normal', 'input-cancel'); 110 | } 111 | }); 112 | 113 | 114 | ////////////////////////////////////////////// 115 | /// 右键呼出热盒 116 | /// 判断的标准是:按下的位置和结束的位置一致 117 | ////////////////////////////////////////////// 118 | var downX, downY; 119 | var MOUSE_RB = 2; // 右键 120 | 121 | container.addEventListener('mousedown', function (e) { 122 | if (e.button == MOUSE_RB) { 123 | e.preventDefault(); 124 | } 125 | if (fsm.state() == 'hotbox') { 126 | hotbox.active(Hotbox.STATE_IDLE); 127 | fsm.jump('normal', 'blur'); 128 | } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) { 129 | downX = e.clientX; 130 | downY = e.clientY; 131 | } 132 | }, false); 133 | 134 | container.addEventListener('mousewheel', function (e) { 135 | if (fsm.state() == 'hotbox') { 136 | hotbox.active(Hotbox.STATE_IDLE); 137 | fsm.jump('normal', 'mousemove-blur'); 138 | } 139 | }, false); 140 | 141 | container.addEventListener('contextmenu', function (e) { 142 | e.preventDefault(); 143 | }); 144 | 145 | container.addEventListener('mouseup', function (e) { 146 | if (fsm.state() != 'normal') { 147 | return; 148 | } 149 | if (e.button != MOUSE_RB || e.clientX != downX || e.clientY != downY) { 150 | return; 151 | } 152 | if (!minder.getSelectedNode()) { 153 | return; 154 | } 155 | fsm.jump('hotbox', 'content-menu'); 156 | }, false); 157 | 158 | // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭 159 | hotbox.$element.addEventListener('mousedown', function (e) { 160 | e.stopPropagation(); 161 | }); 162 | } 163 | 164 | return module.exports = JumpingRuntime; 165 | }); 166 | -------------------------------------------------------------------------------- /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 | var {t} = require("../../locale"); 12 | 13 | function MinderRuntime() { 14 | 15 | // 不使用 kityminder 的按键处理,由 ReceiverRuntime 统一处理 16 | var minder = new Minder({ 17 | enableKeyReceiver: false, 18 | enableAnimation: true 19 | }); 20 | 21 | // 渲染,初始化 22 | minder.renderTo(this.selector); 23 | minder.setTheme(null); 24 | minder.select(minder.getRoot(), true); 25 | minder.execCommand('text', t('minder.main.subject.central')); 26 | 27 | // 导出给其它 Runtime 使用 28 | this.minder = minder; 29 | } 30 | 31 | return module.exports = MinderRuntime; 32 | }); 33 | -------------------------------------------------------------------------------- /src/script/runtime/node.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | function NodeRuntime() { 3 | var runtime = this; 4 | var minder = this.minder; 5 | var hotbox = this.hotbox; 6 | var fsm = this.fsm; 7 | var {t} = require("../../locale"); 8 | 9 | var main = hotbox.state('main'); 10 | var {isDisableNode, markDeleteNode, isDeleteDisableNode} = require('../tool/utils'); 11 | 12 | const buttons = [ 13 | t('minder.menu.move.forward') + ':Alt+Up:ArrangeUp', 14 | t('minder.menu.insert._down') + ':Tab|Insert:AppendChildNode', 15 | t('minder.menu.insert._same') + ':Enter:AppendSiblingNode', 16 | t('minder.menu.move.backward') + ':Alt+Down:ArrangeDown', 17 | t('minder.commons.delete') + ':Delete|Backspace:RemoveNode', 18 | t('minder.menu.insert._up') + ':Shift+Tab|Shift+Insert:AppendParentNode' 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, t('minder.main.subject.branch')); 36 | 37 | function afterAppend() { 38 | if (!--AppendLock) { 39 | runtime.editText(); 40 | } 41 | minder.off('layoutallfinish', afterAppend); 42 | } 43 | minder.on('layoutallfinish', afterAppend); 44 | } else { 45 | if (command.indexOf('RemoveNode') > -1) { 46 | markDeleteNode(minder); 47 | } 48 | minder.execCommand(command); 49 | //fsm.jump('normal', 'command-executed'); 50 | } 51 | }, 52 | enable: function () { 53 | if (command.indexOf("RemoveNode") > -1) { 54 | if (isDeleteDisableNode(minder) && 55 | (command.indexOf("AppendChildNode") < 0 && command.indexOf("AppendSiblingNode") < 0) ) { 56 | return false; 57 | } 58 | } else if(command.indexOf("ArrangeUp") > 0 || command.indexOf("ArrangeDown") > 0) { 59 | if (!minder.moveEnable) { 60 | return false; 61 | } 62 | } else if (command.indexOf("AppendChildNode") < 0 && command.indexOf("AppendSiblingNode") < 0) { 63 | if (isDisableNode(minder)) return false; 64 | } 65 | let node = minder.getSelectedNode(); 66 | if (node && node.parent === null && command.indexOf("AppendSiblingNode") > -1) { 67 | return false; 68 | } 69 | return minder.queryCommandState(command) != -1; 70 | } 71 | }); 72 | }); 73 | 74 | main.button({ 75 | position: 'ring', 76 | key: '/', 77 | action: function () { 78 | if (!minder.queryCommandState('expand')) { 79 | minder.execCommand('expand'); 80 | } else if (!minder.queryCommandState('collapse')) { 81 | minder.execCommand('collapse'); 82 | } 83 | }, 84 | enable: function () { 85 | return minder.queryCommandState('expand') != -1 || minder.queryCommandState('collapse') != -1; 86 | }, 87 | beforeShow: function () { 88 | if (!minder.queryCommandState('expand')) { 89 | this.$button.children[0].innerHTML = t('minder.menu.expand.expand'); 90 | } else { 91 | this.$button.children[0].innerHTML = t('minder.menu.expand.folding'); 92 | } 93 | } 94 | }) 95 | } 96 | 97 | return module.exports = NodeRuntime; 98 | }); 99 | -------------------------------------------------------------------------------- /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 | var {isDisableNode} = require('../tool/utils'); 7 | var {t} = require("../../locale"); 8 | 9 | var main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: t('minder.main.priority'), 14 | key: 'P', 15 | next: 'priority', 16 | enable: function () { 17 | if (isDisableNode(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('priority') != -1; 21 | } 22 | }); 23 | 24 | let priority = hotbox.state('priority') 25 | 26 | priority.button({ 27 | position: 'center', 28 | label: t('minder.commons.remove'), 29 | key: 'Del', 30 | action: function () { 31 | minder.execCommand('Priority', 0); 32 | } 33 | }); 34 | 35 | priority.button({ 36 | position: 'top', 37 | label: t('minder.commons.return'), 38 | key: 'esc', 39 | next: 'back' 40 | }); 41 | 42 | } 43 | return module.exports = PriorityRuntime; 44 | }); 45 | -------------------------------------------------------------------------------- /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 | var {isDisableNode} = require('../tool/utils'); 7 | var {t} = require("../../locale"); 8 | 9 | var main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: t('minder.menu.progress.progress'), 14 | key: 'G', 15 | next: 'progress', 16 | enable: function () { 17 | if (isDisableNode(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('progress') != -1; 21 | } 22 | }); 23 | 24 | var progress = hotbox.state('progress'); 25 | '012345678'.replace(/./g, function (p) { 26 | progress.button({ 27 | position: 'ring', 28 | label: 'G' + p, 29 | key: p, 30 | action: function () { 31 | minder.execCommand('Progress', parseInt(p) + 1); 32 | } 33 | }); 34 | }); 35 | 36 | progress.button({ 37 | position: 'center', 38 | label: t('minder.commons.remove'), 39 | key: 'Del', 40 | action: function () { 41 | minder.execCommand('Progress', 0); 42 | } 43 | }); 44 | 45 | progress.button({ 46 | position: 'top', 47 | label: t('minder.commons.return'), 48 | key: 'esc', 49 | next: 'back' 50 | }); 51 | } 52 | 53 | return module.exports = ProgressRuntime; 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /src/script/runtime/receiver.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var key = require('../tool/key'); 3 | var hotbox = require('./hotbox'); 4 | 5 | function ReceiverRuntime() { 6 | var fsm = this.fsm; 7 | var minder = this.minder; 8 | var me = this; 9 | 10 | // 接收事件的 div 11 | var element = document.createElement('div'); 12 | element.contentEditable = true; 13 | element.setAttribute("tabindex", -1); 14 | element.classList.add('receiver'); 15 | element.onkeydown = element.onkeypress = element.onkeyup = dispatchKeyEvent; 16 | this.container.appendChild(element); 17 | 18 | // receiver 对象 19 | var receiver = { 20 | element: element, 21 | selectAll: function () { 22 | // 保证有被选中的 23 | if (!element.innerHTML) element.innerHTML = ' '; 24 | var range = document.createRange(); 25 | var selection = window.getSelection(); 26 | range.selectNodeContents(element); 27 | selection.removeAllRanges(); 28 | selection.addRange(range); 29 | element.focus(); 30 | }, 31 | enable: function () { 32 | element.setAttribute("contenteditable", true); 33 | }, 34 | disable: function () { 35 | element.setAttribute("contenteditable", false); 36 | }, 37 | fixFFCaretDisappeared: function () { 38 | element.removeAttribute("contenteditable"); 39 | element.setAttribute("contenteditable", "true"); 40 | element.blur(); 41 | element.focus(); 42 | }, 43 | onblur: function (handler) { 44 | element.onblur = handler; 45 | } 46 | }; 47 | receiver.selectAll(); 48 | minder.on('beforemousedown', receiver.selectAll); 49 | minder.on('receiverfocus', receiver.selectAll); 50 | minder.on('readonly', function () { 51 | // 屏蔽minder的事件接受,删除receiver和hotbox 52 | minder.disable(); 53 | editor.receiver.element.parentElement.removeChild(editor.receiver.element); 54 | editor.hotbox.$container.removeChild(editor.hotbox.$element); 55 | }); 56 | 57 | // 侦听器,接收到的事件会派发给所有侦听器 58 | var listeners = []; 59 | 60 | // 侦听指定状态下的事件,如果不传 state,侦听所有状态 61 | receiver.listen = function (state, listener) { 62 | if (arguments.length == 1) { 63 | listener = state; 64 | state = '*'; 65 | } 66 | listener.notifyState = state; 67 | listeners.push(listener); 68 | }; 69 | 70 | function dispatchKeyEvent(e) { 71 | e.is = function (keyExpression) { 72 | var subs = keyExpression.split('|'); 73 | for (var i = 0; i < subs.length; i++) { 74 | if (key.is(this, subs[i])) return true; 75 | } 76 | return false; 77 | }; 78 | var listener, jumpState; 79 | for (var i = 0; i < listeners.length; i++) { 80 | 81 | listener = listeners[i]; 82 | // 忽略不在侦听状态的侦听器 83 | if (listener.notifyState != '*' && listener.notifyState != fsm.state()) { 84 | continue; 85 | } 86 | 87 | if (listener.call(null, e)) { 88 | return; 89 | } 90 | } 91 | } 92 | 93 | this.receiver = receiver; 94 | } 95 | 96 | return module.exports = ReceiverRuntime; 97 | }); 98 | -------------------------------------------------------------------------------- /src/script/runtime/tag.js: -------------------------------------------------------------------------------- 1 | 2 | define(function (require, exports, module) { 3 | 4 | function TagRuntime() { 5 | var minder = this.minder; 6 | var hotbox = this.hotbox; 7 | var {isDisableNode, isTagEnable} = require('../tool/utils'); 8 | var {t} = require("../../locale"); 9 | var main = hotbox.state('main'); 10 | 11 | main.button({ 12 | position: 'top', 13 | label: t('minder.main.tag'), 14 | key: 'H', 15 | next: 'tag', 16 | enable: function () { 17 | if (isDisableNode(minder) && !isTagEnable(minder)) { 18 | return false; 19 | } 20 | return minder.queryCommandState('tag') != -1; 21 | } 22 | }); 23 | 24 | let tag = hotbox.state('tag'); 25 | 26 | tag.button({ 27 | position: 'center', 28 | label: t('minder.commons.remove'), 29 | key: 'Del', 30 | action: function () { 31 | minder.execCommand('Tag', 0); 32 | } 33 | }); 34 | 35 | tag.button({ 36 | position: 'top', 37 | label: t('minder.commons.return'), 38 | key: 'esc', 39 | next: 'back' 40 | }); 41 | } 42 | 43 | return module.exports = TagRuntime; 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /src/script/store.js: -------------------------------------------------------------------------------- 1 | export function setLocalStorage(k, v) { 2 | window.localStorage.setItem(k, JSON.stringify(v)); 3 | } 4 | 5 | export function getLocalStorage(k) { 6 | let v = window.localStorage.getItem(k); 7 | return JSON.parse(v); 8 | } 9 | 10 | export function rmLocalStorage(k) { 11 | window.localStorage.removeItem(k); 12 | } 13 | 14 | export function clearLocalStorage() { 15 | window.localStorage.clear(); 16 | } 17 | -------------------------------------------------------------------------------- /src/script/tool/debug.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | var format = require('./format'); 3 | 4 | function noop() {} 5 | 6 | function stringHash(str) { 7 | var hash = 0; 8 | for (var i = 0; i < str.length; i++) { 9 | hash += str.charCodeAt(i); 10 | } 11 | return hash; 12 | } 13 | 14 | function Debug(flag) { 15 | var debugMode = this.flaged = window.location.search.indexOf(flag) != -1; 16 | 17 | if (debugMode) { 18 | var h = stringHash(flag) % 360; 19 | 20 | var flagStyle = format( 21 | 'background: hsl({0}, 50%, 80%); ' + 22 | 'color: hsl({0}, 100%, 30%); ' + 23 | 'padding: 2px 3px; ' + 24 | 'margin: 1px 3px 0 0;' + 25 | 'border-radius: 2px;', h); 26 | 27 | var textStyle = 'background: none; color: black;'; 28 | this.log = function () { 29 | var output = format.apply(null, arguments); 30 | console.log(format('%c{0}%c{1}', flag, output), flagStyle, textStyle); 31 | }; 32 | } else { 33 | this.log = noop; 34 | } 35 | } 36 | 37 | return module.exports = Debug; 38 | }); 39 | -------------------------------------------------------------------------------- /src/script/tool/format.js: -------------------------------------------------------------------------------- 1 | function format(template, args) { 2 | if (typeof(args) != 'object') { 3 | args = [].slice.call(arguments, 1); 4 | } 5 | return String(template).replace(/\{(\w+)\}/ig, function (match, $key) { 6 | return args[$key] || $key; 7 | }); 8 | } 9 | export {format} 10 | -------------------------------------------------------------------------------- /src/script/tool/innertext.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | if ((!('innerText' in document.createElement('a'))) && ('getSelection' in window)) { 3 | HTMLElement.prototype.__defineGetter__('innerText', function () { 4 | var selection = window.getSelection(), 5 | ranges = [], 6 | str, i; 7 | 8 | for (i = 0; i < selection.rangeCount; i++) { 9 | ranges[i] = selection.getRangeAt(i); 10 | } 11 | 12 | selection.removeAllRanges(); 13 | selection.selectAllChildren(this); 14 | str = selection.toString(); 15 | selection.removeAllRanges(); 16 | for (i = 0; i < ranges.length; i++) { 17 | selection.addRange(ranges[i]); 18 | } 19 | return str; 20 | }); 21 | HTMLElement.prototype.__defineSetter__('innerText', function (text) { 22 | this.innerHTML = (text || '').replace(//g, '>').replace(/\n/g, '
'); 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /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 | 15 | function is(a, b) { 16 | return a && b && hash(a) == hash(b); 17 | } 18 | exports.hash = hash; 19 | exports.is = is; 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 | if ([16, 17, 18, 91].indexOf(keyEvent.keyCode) === -1) { 33 | if (keyEvent.keyCode === 229 && keyEvent.keyIdentifier) { 34 | return hashCode |= parseInt(keyEvent.keyIdentifier.substr(2), 16); 35 | } 36 | hashCode |= keyEvent.keyCode; 37 | } 38 | return hashCode; 39 | } 40 | 41 | function hashKeyExpression(keyExpression) { 42 | var hashCode = 0; 43 | keyExpression.toLowerCase().split(/\s*\+\s*/).forEach(function (name) { 44 | switch (name) { 45 | case 'ctrl': 46 | case 'cmd': 47 | hashCode |= CTRL_MASK; 48 | break; 49 | case 'alt': 50 | hashCode |= ALT_MASK; 51 | break; 52 | case 'shift': 53 | hashCode |= SHIFT_MASK; 54 | break; 55 | default: 56 | hashCode |= keymap[name]; 57 | } 58 | }); 59 | return hashCode; 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /src/script/tool/keymap.js: -------------------------------------------------------------------------------- 1 | define(function (require, exports, module) { 2 | const 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 | for (var key in keymap) { 62 | if (keymap.hasOwnProperty(key)) { 63 | keymap[key.toLowerCase()] = keymap[key]; 64 | } 65 | } 66 | var aKeyCode = 65; 67 | var aCharCode = 'a'.charCodeAt(0); 68 | 69 | 'abcdefghijklmnopqrstuvwxyz'.split('').forEach(function (letter) { 70 | keymap[letter] = aKeyCode + (letter.charCodeAt(0) - aCharCode); 71 | }); 72 | 73 | var n = 9; 74 | do { 75 | keymap[n.toString()] = n + 48; 76 | } while (--n); 77 | 78 | module.exports = keymap; 79 | }); 80 | -------------------------------------------------------------------------------- /src/script/tool/utils.js: -------------------------------------------------------------------------------- 1 | export function isDisableNode(minder) { 2 | let node = undefined; 3 | if (minder && minder.getSelectedNode) { 4 | node = minder.getSelectedNode(); 5 | } 6 | if (node && node.data.disable === true) { 7 | return true; 8 | } 9 | return false; 10 | } 11 | 12 | export function isDeleteDisableNode(minder) { 13 | let node = undefined; 14 | if (minder && minder.getSelectedNode) { 15 | node = minder.getSelectedNode(); 16 | } 17 | if (node && node.data.disable === true && !node.data.allowDelete) { 18 | return true; 19 | } 20 | return false; 21 | } 22 | 23 | export function isTagEnable(minder) { 24 | let node = undefined; 25 | if (minder && minder.getSelectedNode) { 26 | node = minder.getSelectedNode(); 27 | } 28 | if (node && node.data.tagEnable === true) { 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | export function markChangeNode(node) { 35 | if (node && node.data) { 36 | // 修改的该节点标记为 contextChanged 37 | node.data.contextChanged = true; 38 | while (node) { 39 | // 该路径上的节点都标记为 changed 40 | node.data.changed = true; 41 | node = node.parent; 42 | } 43 | } 44 | } 45 | 46 | // 在父节点记录删除的节点 47 | export function markDeleteNode(minder) { 48 | if (minder) { 49 | let nodes = minder.getSelectedNodes(); 50 | nodes.forEach(node => { 51 | if (node && node.parent) { 52 | let pData = node.parent.data; 53 | if (!pData.deleteChild) { 54 | pData.deleteChild = []; 55 | } 56 | _markDeleteNode(node, pData.deleteChild); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | function _markDeleteNode(node, deleteChild) { 63 | deleteChild.push(node.data); 64 | if (node.children) { 65 | node.children.forEach(child => { 66 | _markDeleteNode(child, deleteChild); 67 | }); 68 | } 69 | } 70 | 71 | export function isPriority(e) { 72 | if (e.getAttribute('text-rendering') === 'geometricPrecision' 73 | && e.getAttribute('text-anchor') === 'middle' 74 | ) { 75 | return true; 76 | } 77 | return false; 78 | } 79 | 80 | export function setPriorityView(priorityStartWithZero, priorityPrefix) { 81 | //手动将优先级前面加上P显示 82 | let items = document.getElementsByTagName('text'); 83 | if (items) { 84 | for (let i = 0; i < items.length; i++) { 85 | let item = items[i]; 86 | if (isPriority(item)) { 87 | let content = item.innerHTML; 88 | if (content.indexOf(priorityPrefix) < 0) { 89 | if (priorityStartWithZero) { 90 | content = parseInt(content) - 1 + ''; 91 | } 92 | item.innerHTML = priorityPrefix + content; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * 手动将特定优先级显示到脑图中 101 | * @param {Array} priorities 特定优先级集合 102 | */ 103 | export function setPriorityViewSpecial(priorities) { 104 | let items = document.getElementsByTagName('text'); 105 | if (items) { 106 | for (let i = 0; i < items.length; i++) { 107 | let item = items[i]; 108 | if (isPriority(item)) { 109 | let content = item.innerHTML; 110 | if (priorities[content - 1]) { 111 | item.innerHTML = priorities[content - 1]; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * 将节点及其子节点id置为null,changed 标记为true 120 | * @param node 121 | */ 122 | export function resetNodes(nodes) { 123 | if (nodes) { 124 | nodes.forEach(item => { 125 | if (item.data) { 126 | item.data.id = null; 127 | item.data.contextChanged = true; 128 | item.data.changed = true; 129 | resetNodes(item.children); 130 | } 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /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 | .dropdown-item { 14 | display: inline-block; 15 | width: 50px; 16 | height: 40px; 17 | padding: 0; 18 | margin: 5px; 19 | } 20 | @for $i from 1 through 6 { 21 | .mold-#{$i} { 22 | background-position: (1-$i) * 50px 0; 23 | } 24 | } 25 | } 26 | 27 | .theme-dropdown-list { 28 | width: 160px; 29 | max-height: 400px; 30 | padding-block: 5px; 31 | overflow-y: auto; 32 | &::-webkit-scrollbar { 33 | width: 0; 34 | } 35 | .dropdown-item { 36 | font-size: 12px; 37 | display: inline-block; 38 | text-align: center; 39 | width: 70px; 40 | height: 30px; 41 | line-height: 30px; 42 | padding: 0; 43 | margin: 5px; 44 | } 45 | } 46 | 47 | .expand-dropdown-list { 48 | .dropdown-item { 49 | line-height: 25px; 50 | } 51 | } 52 | 53 | .selection-dropdown-list { 54 | .dropdown-item { 55 | line-height: 25px; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/style/editor.scss: -------------------------------------------------------------------------------- 1 | @import "~@cell-x/kityminder-core/dist/kityminder.core.css"; 2 | @import "navigator.scss"; 3 | @import "hotbox.scss"; 4 | 5 | .km-editor { 6 | overflow: hidden; 7 | z-index: 2; 8 | } 9 | 10 | .km-editor > .mask { 11 | display: block; 12 | position: absolute; 13 | left: 0; 14 | right: 0; 15 | top: 0; 16 | bottom: 0; 17 | background-color: transparent; 18 | } 19 | 20 | .km-editor > .receiver { 21 | position: absolute; 22 | background: white; 23 | outline: none; 24 | box-shadow: 0 0 20px; 25 | left: 0; 26 | top: 0; 27 | padding: 3px 5px; 28 | margin-left: -3px; 29 | margin-top: -5px; 30 | max-width: 300px; 31 | width: auto; 32 | overflow: hidden; 33 | font-size: 14px; 34 | line-height: 1.4em; 35 | min-height: 1.4em; 36 | box-sizing: border-box; 37 | overflow: hidden; 38 | word-break: break-all; 39 | word-wrap: break-word; 40 | border: none; 41 | -webkit-user-select: text; 42 | pointer-events: none; 43 | opacity: 0; 44 | z-index: -1000; 45 | 46 | &.debug { 47 | opacity: 1; 48 | outline: 1px solid green; 49 | background: none; 50 | z-index: 0; 51 | } 52 | 53 | &.input { 54 | pointer-events: all; 55 | opacity: 1; 56 | z-index: 999; 57 | background: white; 58 | outline: none; 59 | } 60 | } 61 | 62 | div.minder-editor-container { 63 | position: absolute; 64 | top: 40px; 65 | bottom: 0; 66 | left: 0; 67 | right: 0; 68 | font-family: Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 69 | } 70 | 71 | .minder-editor { 72 | position: absolute; 73 | top: 92px; 74 | left: 0; 75 | right: 0; 76 | bottom: 0; 77 | } 78 | 79 | .minder-viewer { 80 | position: absolute; 81 | top: 0; 82 | left: 0; 83 | right: 0; 84 | bottom: 0; 85 | } 86 | 87 | .control-panel { 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | width: 250px; 92 | bottom: 0; 93 | border-left: 1px solid #ccc; 94 | } 95 | 96 | .minder-divider { 97 | position: absolute; 98 | top: 0; 99 | right: 250px; 100 | bottom: 0; 101 | width: 2px; 102 | background-color: rgb(251, 251, 251); 103 | cursor: ew-resize; 104 | } 105 | 106 | .hotbox .state .button.enabled.selected .key, 107 | .hotbox .state .ring .key { 108 | margin-top: 5px; 109 | font-size: 13px; 110 | } 111 | 112 | .hotbox .state .bottom .button .label, 113 | .hotbox .state .top .button .label { 114 | font-weight: 600; 115 | } 116 | 117 | .hotbox .exp .ring .button .label { 118 | margin-top: 28px; 119 | margin-left: -2px; 120 | } 121 | 122 | .hotbox .exp .ring .button .key { 123 | display: none; 124 | } 125 | -------------------------------------------------------------------------------- /src/style/header.scss: -------------------------------------------------------------------------------- 1 | @import "mixin.scss"; 2 | @import "dropdown-list.scss"; 3 | header { 4 | font-size: 12px; 5 | * { 6 | box-sizing: content-box; 7 | } 8 | & > ul { 9 | display: flex; 10 | align-items: center; 11 | height: 30px; 12 | margin: 0; 13 | padding: 0; 14 | background-color: #e1e1e1; 15 | li { 16 | line-height: 30px; 17 | display: inline-flex; 18 | width: 80px; 19 | height: 100%; 20 | list-style: none; 21 | a { 22 | font-size: 14px; 23 | width: inherit; 24 | text-align: center; 25 | text-decoration: none; 26 | color: #337ab7; 27 | } 28 | a:hover, 29 | a:focus { 30 | color: #23527c; 31 | } 32 | } 33 | li.selected { 34 | background: #fff; 35 | a { 36 | color: #000; 37 | } 38 | } 39 | } 40 | } 41 | 42 | .mind_tab-content { 43 | border-bottom: 1px solid #eee; 44 | .el-tabs__header { 45 | margin-bottom: 0; 46 | } 47 | } 48 | 49 | .mind-tab-panel { 50 | width: 100%; 51 | height: 100%; 52 | padding-block: 6px; 53 | .menu-container { 54 | display: flex; 55 | min-height: 52px; 56 | & > div { 57 | display: flex; 58 | overflow: hidden; 59 | flex-wrap: wrap; 60 | padding: 4px 10px; 61 | line-height: 12px; 62 | &:not(:last-of-type) { 63 | border-right: 1px dashed #eee; 64 | } 65 | } 66 | .expand-group, 67 | .selection-group { 68 | flex-direction: column; 69 | align-items: center; 70 | justify-content: space-around; 71 | .el-dropdown-link { 72 | display: flex; 73 | gap: 4px; 74 | } 75 | button { 76 | border: none; 77 | outline: none; 78 | padding: 10px 20px; 79 | } 80 | span { 81 | font-size: 12px; 82 | } 83 | } 84 | .expand-group .tab-icons { 85 | background-position: center -995px; 86 | } 87 | .selection-group .tab-icons { 88 | background-position: 7px -1175px; 89 | } 90 | .insert-group { 91 | max-width: 208px; 92 | .menu-btn { 93 | padding: 0 4px; 94 | } 95 | .insert-sibling-box .tab-icons { 96 | background-position: 0 -20px; 97 | } 98 | .insert-parent-box .tab-icons{ 99 | background-position: 0 -40px; 100 | } 101 | } 102 | .move-group, 103 | .edit-del-group { 104 | display: flex; 105 | flex-direction: column; 106 | align-items: center; 107 | justify-content: space-around; 108 | } 109 | .move-group { 110 | .move-up .tab-icons { 111 | background-position: 3px -280px; 112 | } 113 | .move-down .tab-icons { 114 | background-position: 3px -300px; 115 | } 116 | } 117 | .edit-del-group { 118 | .edit .tab-icons { 119 | background-position: 0 -60px; 120 | } 121 | .del .tab-icons { 122 | background-position: 0 -80px; 123 | } 124 | } 125 | } 126 | .menu-btn { 127 | cursor: pointer; 128 | gap: 4px; 129 | @include flexcenter; 130 | } 131 | .menu-btn:not([disabled]):hover { 132 | background-color: $btn-hover-color; 133 | } 134 | .tab-icons { 135 | display: inline-block; 136 | width: 20px; 137 | height: 20px; 138 | background-image: url("../../assets/minder/icons.png"); 139 | background-repeat: no-repeat; 140 | } 141 | .do-group { 142 | width: 40px; 143 | height: 100%; 144 | padding: 0 5px; 145 | p { 146 | height: 50%; 147 | margin: 0; 148 | @include flexcenter; 149 | } 150 | .undo i { 151 | background-position: 0 -1240px; 152 | } 153 | .redo i { 154 | background-position: 0 -1220px; 155 | } 156 | } 157 | .attachment-group { 158 | width: 185px; 159 | @include flexcenter; 160 | .el-dropdown-link { 161 | font-size: 12px; 162 | } 163 | button { 164 | font-size: inherit; 165 | width: 45px; 166 | height: 20px; 167 | padding: 0; 168 | background-repeat: no-repeat; 169 | background-position: right; 170 | @include button; 171 | @include flexcenter; 172 | span { 173 | margin-left: 15px; 174 | } 175 | } 176 | button:hover { 177 | background-color: $btn-hover-color; 178 | } 179 | & > div { 180 | font-size: inherit; 181 | flex-wrap: wrap; 182 | height: 100%; 183 | @include flexcenter; 184 | } 185 | .insert { 186 | height: 25px; 187 | background-repeat: no-repeat; 188 | } 189 | .link { 190 | .insert { 191 | background-position: 50% -100px; 192 | } 193 | } 194 | .img { 195 | .insert { 196 | background-position: 50% -125px; 197 | } 198 | } 199 | .remark { 200 | .insert { 201 | background-position: 50% -1150px; 202 | } 203 | } 204 | .el-dropdown { 205 | cursor: default; 206 | } 207 | } 208 | .progress-group { 209 | ul { 210 | width: 116px; 211 | margin: 0; 212 | padding: 0; 213 | list-style: none; 214 | display: flex; 215 | flex-wrap: wrap; 216 | gap: 4px; 217 | li { 218 | width: 20px; 219 | height: 20px; 220 | transform: scale(1.1); 221 | background-image: url("../../assets/minder/iconprogress.png"); 222 | } 223 | } 224 | @for $i from 0 through 9 { 225 | .progress-#{$i} { 226 | background-position: 0 -20px * (-1 + $i); 227 | } 228 | } 229 | } 230 | .mold-group { 231 | @for $i from 1 through 6 { 232 | .mold-#{$i} { 233 | background-position: (1-$i) * 50px 0; 234 | } 235 | } 236 | .dropdown-toggle { 237 | font-size: 12px; 238 | .current-mold { 239 | display: inline-block; 240 | width: 50px; 241 | height: 42px; 242 | line-height: 42px; 243 | padding: 0; 244 | vertical-align: middle; 245 | } 246 | i { 247 | vertical-align: middle; 248 | } 249 | } 250 | } 251 | .theme-group { 252 | .dropdown-toggle { 253 | font-size: 12px; 254 | .current-theme { 255 | font-size: 12px; 256 | display: inline-block; 257 | text-align: center; 258 | width: 70px; 259 | height: 30px; 260 | line-height: 30px; 261 | padding: 0; 262 | } 263 | } 264 | } 265 | .arrange-group { 266 | .arrange { 267 | display: flex; 268 | flex-direction: column; 269 | align-items: center; 270 | } 271 | .tab-icons { 272 | display: inline-block; 273 | width: 25px; 274 | height: 25px; 275 | margin: 0; 276 | background-repeat: no-repeat; 277 | background-position: 0 -150px; 278 | } 279 | } 280 | .style-group { 281 | align-items: center; 282 | gap: 8px; 283 | .clear-style-btn, 284 | .copy-paste-panel { 285 | display: flex; 286 | flex-direction: column; 287 | align-items: center; 288 | } 289 | .clear-style-btn { 290 | .tab-icons { 291 | display: inline-block; 292 | width: 25px; 293 | height: 25px; 294 | margin: 0; 295 | background-repeat: no-repeat; 296 | background-position: 0 -175px; 297 | } 298 | } 299 | .copy-paste-panel { 300 | gap: 4px; 301 | .tab-icons { 302 | display: inline-block; 303 | width: 20px; 304 | height: 20px; 305 | } 306 | .copy-style { 307 | .tab-icons { 308 | background-position: 0 -200px; 309 | } 310 | } 311 | .paste-style { 312 | .tab-icons { 313 | background-position: 0 -220px; 314 | } 315 | } 316 | } 317 | } 318 | .font-group { 319 | display: flex; 320 | flex-direction: column; 321 | gap: 4px; 322 | .el-input__inner { 323 | width: 80px; 324 | height: 22px !important; 325 | line-height: 22px !important; 326 | } 327 | .el-input__suffix { 328 | right: 0; 329 | } 330 | .el-input__icon { 331 | line-height: 22px !important; 332 | } 333 | .font-bold, 334 | .font-italic { 335 | display: inline-block; 336 | width: 20px; 337 | height: 16px; 338 | margin: 0 3px; 339 | } 340 | .font-bold { 341 | background-position: 0 -242px; 342 | } 343 | .font-italic { 344 | background-position: 0 -262px; 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/style/hotbox.scss: -------------------------------------------------------------------------------- 1 | .hotbox { 2 | font-family: Arial, "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; 3 | position: absolute; 4 | left: 0; 5 | top: 0; 6 | overflow: visible; 7 | 8 | .state { 9 | position: absolute; 10 | overflow: visible; 11 | display: none; 12 | 13 | .center, 14 | .ring { 15 | .button { 16 | position: absolute; 17 | width: 70px; 18 | height: 70px; 19 | margin-left: -35px; 20 | margin-top: -35px; 21 | border-radius: 100%; 22 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); 23 | } 24 | 25 | .label, 26 | .key { 27 | display: block; 28 | text-align: center; 29 | line-height: 1.4em; 30 | vertical-align: middle; 31 | } 32 | 33 | .label { 34 | font-size: 16px; 35 | margin-top: 17px; 36 | color: black; 37 | font-weight: normal; 38 | line-height: 1em; 39 | } 40 | 41 | .key { 42 | font-size: 12px; 43 | color: #999; 44 | } 45 | } 46 | 47 | .ring-shape { 48 | position: absolute; 49 | left: -25px; 50 | top: -25px; 51 | border: 25px solid rgba(0, 0, 0, 0.3); 52 | border-radius: 100%; 53 | box-sizing: content-box; 54 | } 55 | 56 | .top, 57 | .bottom { 58 | position: absolute; 59 | white-space: nowrap; 60 | 61 | .button { 62 | display: inline-block; 63 | padding: 8px 15px; 64 | margin: 0 10px; 65 | border-radius: 15px; 66 | box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); 67 | position: relative; 68 | 69 | .label { 70 | font-size: 14px; 71 | line-height: 14px; 72 | vertical-align: middle; 73 | color: black; 74 | line-height: 1em; 75 | } 76 | 77 | .key { 78 | font-size: 12px; 79 | line-height: 12px; 80 | vertical-align: middle; 81 | color: #999; 82 | margin-left: 3px; 83 | 84 | &:before { 85 | content: "("; 86 | } 87 | 88 | &:after { 89 | content: ")"; 90 | } 91 | } 92 | } 93 | } 94 | 95 | .button { 96 | background: #f9f9f9; 97 | overflow: hidden; 98 | cursor: default; 99 | 100 | .key, 101 | .label { 102 | opacity: 0.3; 103 | } 104 | } 105 | 106 | .button.enabled { 107 | background: white; 108 | 109 | .key, 110 | .label { 111 | opacity: 1; 112 | } 113 | 114 | &:hover { 115 | background: lighten(rgb(228, 93, 92), 5%); 116 | 117 | .label { 118 | color: white; 119 | } 120 | 121 | .key { 122 | color: lighten(rgb(228, 93, 92), 30%); 123 | } 124 | } 125 | 126 | &.selected { 127 | -webkit-animation: selected 0.1s ease; 128 | background: rgb(228, 93, 92); 129 | 130 | .label { 131 | color: white; 132 | } 133 | 134 | .key { 135 | color: lighten(rgb(228, 93, 92), 30%); 136 | } 137 | } 138 | 139 | &.pressed, 140 | &:active { 141 | background: #ff974d; 142 | 143 | .label { 144 | color: white; 145 | } 146 | 147 | .key { 148 | color: lighten(#ff974d, 30%); 149 | } 150 | } 151 | } 152 | } 153 | 154 | .state.active { 155 | display: block; 156 | } 157 | } 158 | 159 | @-webkit-keyframes selected { 160 | 0% { 161 | transform: scale(1); 162 | } 163 | 164 | 50% { 165 | transform: scale(1.1); 166 | } 167 | 168 | 100% { 169 | transform: scale(1); 170 | } 171 | } 172 | 173 | .hotbox-key-receiver { 174 | position: absolute; 175 | left: -999999px; 176 | top: -999999px; 177 | width: 20px; 178 | height: 20px; 179 | outline: none; 180 | margin: 0; 181 | } 182 | -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | $btn-hover-color: #eee; 2 | *[disabled] { 3 | opacity: 0.5; 4 | } 5 | 6 | @mixin block { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | @mixin button { 12 | background: transparent; 13 | border: none; 14 | outline: none; 15 | } 16 | 17 | @mixin flexcenter { 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | } 22 | -------------------------------------------------------------------------------- /src/style/navigator.scss: -------------------------------------------------------------------------------- 1 | .nav-bar { 2 | position: absolute; 3 | width: 35px; 4 | height: 200px; 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 | width: 20px; 24 | height: 20px; 25 | margin: 2px auto; 26 | display: block; 27 | } 28 | 29 | &.active { 30 | background-color: #5a6378; 31 | } 32 | } 33 | 34 | .zoom-in .icon { 35 | background-position: 0 -730px; 36 | } 37 | 38 | .zoom-out .icon { 39 | background-position: 0 -750px; 40 | } 41 | 42 | .hand .icon { 43 | background-position: 0 -770px; 44 | width: 25px; 45 | height: 25px; 46 | margin: 0 auto; 47 | } 48 | 49 | .camera .icon { 50 | background-position: 0 -870px; 51 | width: 25px; 52 | height: 25px; 53 | margin: 0 auto; 54 | } 55 | 56 | .nav-trigger .icon { 57 | background-position: 0 -845px; 58 | width: 25px; 59 | height: 25px; 60 | margin: 0 auto; 61 | } 62 | 63 | .zoom-pan { 64 | width: 2px; 65 | height: 70px; 66 | box-shadow: 0 1px #e50000; 67 | position: relative; 68 | background: white; 69 | margin: 3px auto; 70 | overflow: visible; 71 | 72 | .origin { 73 | position: absolute; 74 | width: 20px; 75 | height: 8px; 76 | left: -9px; 77 | margin-top: -4px; 78 | background: transparent; 79 | 80 | &:after { 81 | content: " "; 82 | display: block; 83 | width: 6px; 84 | height: 2px; 85 | background: white; 86 | left: 7px; 87 | top: 3px; 88 | position: absolute; 89 | } 90 | } 91 | 92 | .indicator { 93 | position: absolute; 94 | width: 8px; 95 | height: 8px; 96 | left: -3px; 97 | background: white; 98 | border-radius: 100%; 99 | margin-top: -4px; 100 | } 101 | } 102 | } 103 | 104 | .nav-previewer { 105 | background: #fff; 106 | width: 140px; 107 | height: 120px; 108 | position: absolute; 109 | left: 45px; 110 | bottom: 30px; 111 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 112 | border-radius: 0 2px 2px 0; 113 | padding: 1px; 114 | z-index: 9; 115 | cursor: crosshair; 116 | transition: -webkit-transform 0.7s 0.1s ease; 117 | transition: transform 0.7s 0.1s ease; 118 | 119 | &.grab { 120 | cursor: move; 121 | cursor: -webkit-grabbing; 122 | cursor: -moz-grabbing; 123 | cursor: grabbing; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/style/normalize.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | line-height: 1.15; 4 | -ms-text-size-adjust: 100%; 5 | -webkit-text-size-adjust: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | } 11 | 12 | article, aside, footer, header, nav, section { 13 | display: block; 14 | } 15 | 16 | h1 { 17 | font-size: 2em; 18 | margin: .67em 0; 19 | } 20 | 21 | figcaption, figure, main { 22 | display: block; 23 | } 24 | 25 | figure { 26 | margin: 1em 40px; 27 | } 28 | 29 | hr { 30 | overflow: visible; 31 | box-sizing: content-box; 32 | height: 0; 33 | } 34 | 35 | pre { 36 | font-family: monospace, monospace; 37 | font-size: 1em; 38 | } 39 | 40 | a { 41 | background-color: transparent; 42 | -webkit-text-decoration-skip: objects; 43 | } 44 | 45 | a:active, a:hover { 46 | outline-width: 0; 47 | } 48 | 49 | abbr[title] { 50 | text-decoration: underline; 51 | text-decoration: underline dotted; 52 | border-bottom: none; 53 | } 54 | 55 | b, strong { 56 | font-weight: inherit; 57 | } 58 | 59 | b, strong { 60 | font-weight: bolder; 61 | } 62 | 63 | code, kbd, samp { 64 | font-family: monospace, monospace; 65 | font-size: 1em; 66 | } 67 | 68 | dfn { 69 | font-style: italic; 70 | } 71 | 72 | mark { 73 | color: #000; 74 | background-color: #ff0; 75 | } 76 | 77 | small { 78 | font-size: 80%; 79 | } 80 | 81 | sub, sup { 82 | font-size: 75%; 83 | line-height: 0; 84 | position: relative; 85 | vertical-align: baseline; 86 | } 87 | 88 | sub { 89 | bottom: -.25em; 90 | } 91 | 92 | sup { 93 | top: -.5em; 94 | } 95 | 96 | audio, video { 97 | display: inline-block; 98 | } 99 | 100 | audio:not([controls]) { 101 | display: none; 102 | height: 0; 103 | } 104 | 105 | img { 106 | border-style: none; 107 | } 108 | 109 | svg:not(:root) { 110 | overflow: hidden; 111 | } 112 | 113 | button, input, optgroup, select, textarea { 114 | font-family: sans-serif; 115 | font-size: 100%; 116 | line-height: 1.15; 117 | margin: 0; 118 | } 119 | 120 | button, input { 121 | overflow: visible; 122 | } 123 | 124 | button, select { 125 | text-transform: none; 126 | } 127 | 128 | button, html [type='button'], [type='reset'], [type='submit'] { 129 | -webkit-appearance: button; 130 | } 131 | 132 | [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner, button::-moz-focus-inner { 133 | padding: 0; 134 | border-style: none; 135 | } 136 | 137 | [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring, button:-moz-focusring { 138 | outline: 1px dotted ButtonText; 139 | } 140 | 141 | fieldset { 142 | margin: 0 2px; 143 | padding: .35em .625em .75em; 144 | border: 1px solid #c0c0c0; 145 | } 146 | 147 | legend { 148 | display: table; 149 | box-sizing: border-box; 150 | max-width: 100%; 151 | padding: 0; 152 | white-space: normal; 153 | color: inherit; 154 | } 155 | 156 | progress { 157 | display: inline-block; 158 | vertical-align: baseline; 159 | } 160 | 161 | textarea { 162 | overflow: auto; 163 | } 164 | 165 | [type='checkbox'], [type='radio'] { 166 | box-sizing: border-box; 167 | padding: 0; 168 | } 169 | 170 | [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { 171 | height: auto; 172 | } 173 | 174 | [type='search'] { 175 | outline-offset: -2px; 176 | -webkit-appearance: textfield; 177 | } 178 | 179 | [type='search']::-webkit-search-cancel-button, [type='search']::-webkit-search-decoration { 180 | -webkit-appearance: none; 181 | } 182 | 183 | ::-webkit-file-upload-button { 184 | font: inherit; 185 | -webkit-appearance: button; 186 | } 187 | 188 | details, menu { 189 | display: block; 190 | } 191 | 192 | summary { 193 | display: list-item; 194 | } 195 | 196 | canvas { 197 | display: inline-block; 198 | } 199 | 200 | template { 201 | display: none; 202 | } 203 | 204 | [hidden] { 205 | display: none; 206 | } 207 | --------------------------------------------------------------------------------