├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .postcssrc.js ├── 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 ├── dist └── vue-draggable-tree.min.js ├── draggable-tree.gif ├── examples ├── basic.vue ├── custom.vue ├── index.html └── static │ ├── arrow.svg │ ├── css │ └── app.0df69f162c2b59b459c1f36f049df765.css │ └── js │ ├── app.c7898957126c69612825.js │ ├── app.c7898957126c69612825.js.map │ ├── manifest.7b5e097f4d45823be1ad.js │ ├── manifest.7b5e097f4d45823be1ad.js.map │ ├── vendor.f421c594ed3c4b2bc8a7.js │ └── vendor.f421c594ed3c4b2bc8a7.js.map ├── index.html ├── package.json ├── src ├── App.vue ├── assets │ ├── arrow.svg │ └── logo.png ├── components │ ├── Highlight.vue │ ├── Index.vue │ ├── basic.vue │ ├── custom.vue │ └── hooks.vue ├── main.js ├── test │ ├── customData.js │ └── utils.test.js └── tree │ ├── Tree.js │ ├── TreeNode.js │ ├── arrow.svg │ ├── constants.js │ ├── defaultTemplate.js │ ├── index.js │ ├── iviewTemplate.js │ ├── style.css │ └── utils.js ├── static └── arrow.svg ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/transform-runtime", 7 | "babel-plugin-syntax-jsx", 8 | "transform-vue-jsx" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | extends: 'airbnb-base', 13 | globals: { 14 | jest: false, 15 | describe: false, 16 | it: false, 17 | expect: false, 18 | }, 19 | // required to lint *.vue files 20 | plugins: [ 21 | 'html' 22 | ], 23 | // check if imports actually resolve 24 | 'settings': { 25 | 'import/resolver': { 26 | 'webpack': { 27 | 'config': 'build/webpack.base.conf.js' 28 | } 29 | } 30 | }, 31 | // add your custom rules here 32 | 'rules': { 33 | // don't require .vue extension when importing 34 | 'import/extensions': ['error', 'always', { 35 | 'js': 'never', 36 | 'vue': 'never' 37 | }], 38 | // allow optionalDependencies 39 | 'import/no-extraneous-dependencies': ['error', { 40 | 'optionalDependencies': ['test/unit/index.js'] 41 | }], 42 | // allow debugger during development 43 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 44 | indent: 0, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Editor directories and files 8 | .idea 9 | .vscode 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | index.html 2 | src 3 | build 4 | config 5 | examples 6 | .babelrc 7 | .editorconfig 8 | .eslintignore 9 | .gitignore 10 | webpack.config.js 11 | *.log 12 | yarn.lock 13 | index.js 14 | .postcssrc.js 15 | .eslintrc.js 16 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-draggable-tree 2 | 3 | > 最容易使用的 vue 可拖拽树。 4 | 5 | ![效果图](./draggable-tree.gif) 6 | 7 | 参考~~抄袭~~`rc-tree`实现的一个可拖拽树,样式参考`ant-design`。 8 | 9 | ## Example 10 | 11 | online example: https://ltaoo.github.io/vue-draggable-tree/examples/index.html 12 | 13 | ## Install 14 | 15 | ```bash 16 | yarn add vue-draggable-tree 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | 29 | 30 | 119 | 120 | 123 | ``` 124 | 125 | ## API 126 | 127 | 属性 | 说明 | 类型 | 默认值 | 128 | ---|---|---|---| 129 | data | 要渲染的数据 | Array | 空 130 | draggable | 设置节点可拖拽(IE>8)| Boolean | false 131 | dragEnd | 拖拽结束后调用的事件 | function(ary, node, e) | - 132 | onDragEnter | dragenter 触发时调用 | function({event, node}) | - 133 | onDragLeave | dragleave 触发时调用 | function({event, node}) | - 134 | onDragOver | dragover 触发时调用 | function({event, node}) | - 135 | onDragStart | dragstart 触发时调用 | function({event, node}) | - 136 | onDrop | drop 触发时调用 | function({event, node}) | - 137 | onExpand | 展开/收起节点时触发 | function({event, node}) | - 138 | afterInsert | 在节点插入到指定位置后调用 | function() | - 139 | template | 自定义节点内容 | VueComponent | - 140 | 141 | ## 说明 142 | 树组件,分为两部分吧,首先是渲染,其次是渲染后处理拖动。 143 | 144 | ### render 145 | 渲染其实比较简单,首先我们有「元数据」,我们定义下接口 146 | 147 | ```typescript 148 | interface SourceNode { 149 | key: string; 150 | title: string; 151 | children?: Array; 152 | [propName: string]: any; 153 | } 154 | ``` 155 | 156 | `key` 需要是唯一值。然后根据这个元数据生成 `vnode`,`Tree` 组件要求 `data` 是数组,所以是生成了 `vnode` 组成的数组。 157 | 再使用 `this.renderTreeNode` 真正地去渲染。 158 | 159 | ```vue 160 | // vChildren Array,h createElement 161 | const vChildren = loop(this.data, h, TreeNode); 162 | 169 | ``` 170 | 171 | `VNode` 的接口长什么样呢? 172 | 173 | ```js 174 | interface VNode { 175 | tag: string; 176 | data: Object; 177 | children: Array; 178 | // ... 179 | } 180 | ``` 181 | 182 | 在 `React` 中,`JSX` 其实是创建 `React Element` 的语法糖,这里也是类似, 183 | 184 | 参考 [渲染函数 & JSX](https://cn.vuejs.org/v2/guide/render-function.html#JSX) 185 | 186 | ### drop 187 | 该组件核心概念在于,所有 `TreeNode`,无论层级多深,都是由 `Tree` 这个根组件去管理。每个节点有「位置」属性,类似 0、0-0 这样,个数表示层级,第二位数字表示在该层级的位置,如 '0-1' 表示第一层级的第二个节点。 188 | 189 | ```js 190 | - 图书 191 | - 经管 192 | - 创京东 193 | - 参与感 194 | - 服装 <----- 这个节点就是 0-1 195 | ``` 196 | 197 | 当拖动 `Node` 时,会触发相关事件 198 | 199 | - dragstart 200 | - dragenter 201 | - dragover 202 | - drop 203 | - dragend 204 | 205 | `dragstart`,当拖动节点时调用;`dragenter`,当拖动时「被进入」节点触发,举例,拖动「三体」到「经管」这个 `node` 时,触发 `dragenter` 的是「经管」节点,而不是「三体」节点; 206 | 207 | 并且,当 `enter` 时,这里有一个黑科技,`node` 的实际大小要大于我们所看到的大小。当我们移动到「下边缘」时,实际上是已经移到了内部,只是我们看起来还没有到内部,通过鼠标位置与节点位置的计算,我们人为地划分了「上边缘」、「内部」和「下边缘」。 208 | 209 | 计算方式是先获取到目标节点距离屏幕顶部的距离(top)、目标节点高度(height)、当前鼠标距屏幕顶部距离(y),如果 210 | - 1、y < top 说明鼠标在节点上方 211 | - 2、y > top + height 说明鼠标在节点下方 212 | - 3、其他情况说明在节点内部 213 | 214 | ### 进阶 215 | 216 | 定制渲染内容 217 | 218 | ### 需要注意的问题 219 | 220 | 当在同组间移动时,先移除原先的,再插入,位置计算会有问题。 221 | 222 | ## 发布流程 223 | 224 | 如果对组件本身有修改,修改完成后更新 `package.json` 中的 `version` 字段,并且需要打包`example` 和 `dist`,分别运行 225 | 226 | ```bash 227 | yarn build 228 | ``` 229 | 230 | ```bash 231 | yarn dist 232 | ``` 233 | 234 | 然后可以先将新的包发布到 `npm` 上,运行: 235 | 236 | ```bash 237 | npm publish 238 | ``` 239 | 240 | 然后将源代码 `push` 到 `github`。 241 | 242 | > 如果只修改了 `example`,只需要 `build` 后 `push` 源码即可。 243 | 244 | ## todo 245 | 246 | - []优化发布流程 247 | - [x]代码整理 248 | - []examples 展示页用例完善 249 | - []增加 theme 以方便直接在 iview 或者 element 项目中使用 250 | - []增加 checkbox 251 | - []选中状态 252 | - []增加连接线 253 | - []增加禁用状态 254 | - []是否展开控制 255 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | function exec (cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | const versionRequirements = [ 11 | { 12 | name: 'node', 13 | currentVersion: semver.clean(process.version), 14 | versionRequirement: packageConfig.engines.node 15 | } 16 | ] 17 | 18 | if (shell.which('npm')) { 19 | versionRequirements.push({ 20 | name: 'npm', 21 | currentVersion: exec('npm --version'), 22 | versionRequirement: packageConfig.engines.npm 23 | }) 24 | } 25 | 26 | module.exports = function () { 27 | const warnings = [] 28 | for (let i = 0; i < versionRequirements.length; i++) { 29 | const mod = versionRequirements[i] 30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 31 | warnings.push(mod.name + ': ' + 32 | chalk.red(mod.currentVersion) + ' should be ' + 33 | chalk.green(mod.versionRequirement) 34 | ) 35 | } 36 | } 37 | 38 | if (warnings.length) { 39 | console.log('') 40 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 41 | console.log() 42 | for (let i = 0; i < warnings.length; i++) { 43 | const warning = warnings[i] 44 | console.log(' ' + warning) 45 | } 46 | console.log() 47 | process.exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 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 | 16 | // default port where dev server listens for incoming traffic 17 | const port = process.env.PORT || config.dev.port 18 | // automatically open browser, if not set will be false 19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 20 | // Define HTTP proxies to your custom API backend 21 | // https://github.com/chimurai/http-proxy-middleware 22 | const proxyTable = config.dev.proxyTable 23 | 24 | const app = express() 25 | const compiler = webpack(webpackConfig) 26 | 27 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 28 | publicPath: webpackConfig.output.publicPath, 29 | quiet: true 30 | }) 31 | 32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 33 | log: false, 34 | heartbeat: 2000 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | // currently disabled until this is resolved: 38 | // https://github.com/jantimon/html-webpack-plugin/issues/680 39 | // compiler.plugin('compilation', function (compilation) { 40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 41 | // hotMiddleware.publish({ action: 'reload' }) 42 | // cb() 43 | // }) 44 | // }) 45 | 46 | // enable hot-reload and state-preserving 47 | // compilation error display 48 | app.use(hotMiddleware) 49 | 50 | // proxy api requests 51 | Object.keys(proxyTable).forEach(function (context) { 52 | let options = proxyTable[context] 53 | if (typeof options === 'string') { 54 | options = { target: options } 55 | } 56 | app.use(proxyMiddleware(options.filter || context, options)) 57 | }) 58 | 59 | // handle fallback for HTML5 history API 60 | app.use(require('connect-history-api-fallback')()) 61 | 62 | // serve webpack bundle output 63 | app.use(devMiddleware) 64 | 65 | // serve pure static assets 66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 67 | app.use(staticPath, express.static('./static')) 68 | 69 | const uri = 'http://localhost:' + port 70 | 71 | var _resolve 72 | var _reject 73 | var readyPromise = new Promise((resolve, reject) => { 74 | _resolve = resolve 75 | _reject = reject 76 | }) 77 | 78 | var server 79 | var portfinder = require('portfinder') 80 | portfinder.basePort = port 81 | 82 | console.log('> Starting dev server...') 83 | devMiddleware.waitUntilValid(() => { 84 | portfinder.getPort((err, port) => { 85 | if (err) { 86 | _reject(err) 87 | } 88 | process.env.PORT = port 89 | var uri = 'http://localhost:' + port 90 | console.log('> Listening at ' + uri + '\n') 91 | // when env is testing, don't need open it 92 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 93 | opn(uri) 94 | } 95 | server = app.listen(port) 96 | _resolve() 97 | }) 98 | }) 99 | 100 | module.exports = { 101 | ready: readyPromise, 102 | close: () => { 103 | server.close() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | exports.assetsPath = function (_path) { 7 | const 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.cssLoaders = function (options) { 14 | options = options || {} 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | minimize: process.env.NODE_ENV === 'production', 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | // generate loader string to be used with extract text plugin 25 | function generateLoaders (loader, loaderOptions) { 26 | const loaders = [cssLoader] 27 | if (loader) { 28 | loaders.push({ 29 | loader: loader + '-loader', 30 | options: Object.assign({}, loaderOptions, { 31 | sourceMap: options.sourceMap 32 | }) 33 | }) 34 | } 35 | 36 | // Extract CSS when that option is specified 37 | // (which is the case during production build) 38 | if (options.extract) { 39 | return ExtractTextPlugin.extract({ 40 | use: loaders, 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', { indentedSyntax: true }), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | const output = [] 63 | const loaders = exports.cssLoaders(options) 64 | for (const extension in loaders) { 65 | const loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | 'vue$': 'vue/dist/vue.esm.js', 26 | '@': resolve('src'), 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | enforce: 'pre', 35 | include: [resolve('src'), resolve('test')], 36 | options: { 37 | formatter: require('eslint-friendly-formatter') 38 | } 39 | }, 40 | { 41 | test: /\.vue$/, 42 | loader: 'vue-loader', 43 | options: vueLoaderConfig 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src'), resolve('test')] 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | loader: 'url-loader', 53 | options: { 54 | limit: 10000, 55 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 56 | } 57 | }, 58 | { 59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 60 | loader: 'url-loader', 61 | options: { 62 | limit: 10000, 63 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 64 | } 65 | }, 66 | { 67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 68 | loader: 'url-loader', 69 | options: { 70 | limit: 10000, 71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 72 | } 73 | } 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 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 HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | // add hot-reload related code to entry chunks 11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 13 | }) 14 | 15 | module.exports = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 18 | }, 19 | // cheap-module-eval-source-map is faster for development 20 | devtool: '#cheap-module-eval-source-map', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': config.dev.env 24 | }), 25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new FriendlyErrorsPlugin() 35 | ] 36 | }) 37 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | 13 | const env = config.build.env 14 | 15 | const webpackConfig = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ 18 | sourceMap: config.build.productionSourceMap, 19 | extract: true 20 | }) 21 | }, 22 | devtool: config.build.productionSourceMap ? '#source-map' : false, 23 | output: { 24 | path: config.build.assetsRoot, 25 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 27 | }, 28 | plugins: [ 29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 30 | new webpack.DefinePlugin({ 31 | 'process.env': env 32 | }), 33 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: config.build.index, 56 | template: 'index.html', 57 | inject: true, 58 | minify: { 59 | removeComments: true, 60 | collapseWhitespace: true, 61 | removeAttributeQuotes: true 62 | // more options: 63 | // https://github.com/kangax/html-minifier#options-quick-reference 64 | }, 65 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 66 | chunksSortMode: 'dependency' 67 | }), 68 | // keep module.id stable when vender modules does not change 69 | new webpack.HashedModuleIdsPlugin(), 70 | // split vendor js into its own file 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'vendor', 73 | minChunks: function (module) { 74 | // any required modules inside node_modules are extracted to vendor 75 | return ( 76 | module.resource && 77 | /\.js$/.test(module.resource) && 78 | module.resource.indexOf( 79 | path.join(__dirname, '../node_modules') 80 | ) === 0 81 | ) 82 | } 83 | }), 84 | // extract webpack runtime and module manifest to its own file in order to 85 | // prevent vendor hash from being updated whenever app bundle is updated 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'manifest', 88 | chunks: ['vendor'] 89 | }), 90 | // copy custom static assets 91 | new CopyWebpackPlugin([ 92 | { 93 | from: path.resolve(__dirname, '../static'), 94 | to: config.build.assetsSubDirectory, 95 | ignore: ['.*'] 96 | } 97 | ]) 98 | ] 99 | }) 100 | 101 | if (config.build.productionGzip) { 102 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 103 | 104 | webpackConfig.plugins.push( 105 | new CompressionWebpackPlugin({ 106 | asset: '[path].gz[query]', 107 | algorithm: 'gzip', 108 | test: new RegExp( 109 | '\\.(' + 110 | config.build.productionGzipExtensions.join('|') + 111 | ')$' 112 | ), 113 | threshold: 10240, 114 | minRatio: 0.8 115 | }) 116 | ) 117 | } 118 | 119 | if (config.build.bundleAnalyzerReport) { 120 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 122 | } 123 | 124 | module.exports = webpackConfig 125 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.3 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../examples/index.html'), 12 | assetsRoot: path.resolve(__dirname, '../examples'), 13 | assetsSubDirectory: 'static', 14 | // assetsPublicPath: '/', 15 | productionSourceMap: true, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: process.env.PORT || 8080, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: {}, 35 | // CSS Sourcemaps off by default because relative paths are "buggy" 36 | // with this option, according to the CSS-Loader README 37 | // (https://github.com/webpack/css-loader#sourcemaps) 38 | // In our experience, they generally work as expected, 39 | // just be aware of this issue when enabling this option. 40 | cssSourceMap: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /dist/vue-draggable-tree.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("vue"),require("classnames")):"function"==typeof define&&define.amd?define(["vue","classnames"],t):"object"==typeof exports?exports.VueDraggableTree=t(require("vue"),require("classnames")):e.VueDraggableTree=t(e.vue,e.classnames)}("undefined"!=typeof self?self:this,function(e,t){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=3)}([function(t,n){t.exports=e},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r={TOP:1,BOTTOM:-1,CONTENT:0}},function(e,t,n){"use strict";function r(){}function o(){function e(t,r,o,i){var a=t;t&&t.length&&(a=t.filter(Boolean)),a.forEach(function(t,r){if(t.isTreeNode){var a=t.pos;o.push(a);var s=[];t.$children&&e(t.$children,a,s,a),n(t,r,a,t.rckey||a,s,i)}})}var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],n=arguments.length>1?arguments[1]:void 0;e(t,0,[])}function i(e,t){return e.every(function(e,n){return e===t[n]})}function a(e){var t=[],n=e.pos.split("-");return o(e.$children,function(r,o,a,s){var c=a.split("-");(e.pos===a||n.lengthr+o-2?y.a.BOTTOM:ie?(o.splice(r,1),t.splice(e,0,n),{targetSourceNodes:t,originSourceNodes:o}):(t.splice(e,0,n),o.splice(r,1),{targetSourceNodes:t,originSourceNodes:o})}function l(e,t,n,r,o){var i=e+1;return o===t&&e>r&&(i=e),o.splice(r,1),t.splice(i,0,n),{targetSourceNodes:t,originSourceNodes:o}}t.h=r,n.d(t,"d",function(){return v}),t.e=a,t.a=c,n.d(t,"c",function(){return N}),t.b=u,t.g=d,t.f=l;var p=n(14),f=n.n(p),g=n(16),h=n.n(g),y=n(1),v=function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,r=arguments.length>2?arguments[2]:void 0;return t.map(function(t,o){var i=t.key,a=t.title,s=h()(t,["key","title"]),c=f()({},s,{key:i,title:a,pos:void 0===r?String(o):"".concat(r,"-").concat(o)});if(t.children&&t.children.length){var u=n+1;c.children=e(t.children,u,c.pos)}return c})},N=function e(t,n,r){t.forEach(function(t,o,i){return t.key===n?r(t,o,i):!!t.children&&e(t.children,n,r)})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(4),o=n(2);n.d(t,"computeMoveNeededParams",function(){return o.b}),n.d(t,"findSourceNodeByKey",function(){return o.c}),n.d(t,"insertToBottom",function(){return o.f}),n.d(t,"insertToTop",function(){return o.g});var i=n(1);n.d(t,"TARGET_POSITION_TYPE",function(){return i.a}),t.default=r.a},function(e,t,n){"use strict";var r=n(5),o=n.n(r),i=n(0),a=n.n(i),s=n(9),c=n(13),u=n(1),d=n(2),l=n(18);n.n(l);t.a=a.a.component("Tree",{props:{value:{type:Array},autoExpandParent:{type:Boolean,default:!0},draggable:{type:Boolean,default:!1},onDragEnd:{type:Function,default:d.h},onDragEnter:{type:Function,default:d.h},onDragLeave:{type:Function,default:d.h},onDragOver:{type:Function,default:d.h},onDragStart:{type:Function,default:d.h},onDrop:{type:Function},onExpand:{type:Function,default:d.h},template:{type:Function,default:c.a},beforeInner:{type:Function},beforeInsert:{type:Function},afterInsert:{type:Function}},data:function(){return this.draggingNodesKeys=[],{dragOverNodeKey:"",dropPosition:"",expandedKeys:[]}},computed:{data:function(){return this.value}},methods:{renderTreeNode:function(e){var t=e.key,n=e.title,r=e.pos,o=e.children,i=this.dragOverNodeKey===t&&this.dropPosition===u.a.TOP,a=this.dragOverNodeKey===t&&this.dropPosition===u.a.BOTTOM,c=this.dragOverNodeKey===t&&this.dropPosition===u.a.CONTENT,d=-1!==this.expandedKeys.indexOf(t);return this.$createElement(s.a,{props:{rckey:t,title:n,root:this,pos:r,children:o,dragOver:c,dragOverGapTop:i,dragOverGapBottom:a,template:this.template,draggable:this.draggable,expanded:d,source:e}})},handleStartDrag:function(e,t){this.draggingNode=t,this.draggingNodesKeys=Object(d.e)(t),this.onDragStart({event:e,node:t})},handleNodeEntered:function(e,t){var n=Object(d.a)(e,t);if(this.draggingNode.rckey===t.rckey&&0===n)return this.dragOverNodeKey="",void(this.dropPosition=null);this.dragOverNodeKey=t.rckey,this.dropPosition=n,this.onDragEnter({event:e,node:t})},handleNodeCrossed:function(e,t){this.onDragOver({event:e,node:t})},handleNodeLeaved:function(e,t){this.onDragLeave({event:e,node:t})},handleNodeDropped:function(e,t){var n=t.rckey,r=this.dropPosition;if(this.dragOverNodeKey="",this.draggingNodesKeys.includes(n))return void console.error("Can not drop to dragNode(include it's children node)");var i={event:e,node:t,dragNode:this.draggingNode,targetPosition:r};if(r!==u.a.CONTENT&&(i.dropToGap=!0),this.onDrop)return void this.onDrop(i);var a=n,s=this.draggingNode.rckey,c=o()(this.data),l=Object(d.b)(c,s,a,r),p=l.targetSourceNode,f=l.targetSourceNodeIndex,g=l.targetSourceNodes,h=l.originSourceNode,y=l.originSourceNodeIndex,v=l.originSourceNodes;if(r===u.a.CONTENT){if(this.beforeInner)return void this.beforeInner("inner",p.children,h);p.children=p.children||[],p.children.push(h),v.splice(y,1)}if(this.beforeInsert)return void this.beforeInsert("insert",g,f,h);r===u.a.TOP&&Object(d.g)(f,g,h,y,v),r===u.a.BOTTOM&&Object(d.f)(f,g,h,y,v),this.$emit("input",c),this.afterInsert&&this.afterInsert()},handleDragEnd:function(e,t){this.dragOverNodeKey="",this.$emit("dragEnd",this.data,t,e)},expand:function(e){var t=!e.expanded,n=o()(this.expandedKeys),r=e.rckey,i=n.indexOf(r);t&&-1===i?n.push(r):!t&&i>-1&&n.splice(i,1),this.expandedKeys=n,this.onExpand(n,{node:e,expanded:t})}},render:function(){return(0,arguments[0])("ul",{class:"ant-tree tree",attrs:{role:"tree-node",unselectable:"on"}},[Object(d.d)(this.data).map(this.renderTreeNode)])}})},function(e,t,n){function r(e){return o(e)||i(e)||a()}var o=n(6),i=n(7),a=n(8);e.exports=r},function(e,t){function n(e){if(Array.isArray(e)){for(var t=0,n=new Array(e.length);t0&&this.expanded&&(t+=" ivu-tree-arrow-open"),e("span",{on:{click:this.onExpand},class:"ivu-tree-arrow".concat(t)},[this.children&&this.children.length?e("img",{style:"width: 16px; vertical-align: bottom;",attrs:{src:d.a}}):null])}},render:function(e){var t=this,n="";return this.disabled||(this.dragOver?n="drag-over":this.dragOverGapTop?n="drag-over-gap-top":this.dragOverGapBottom&&(n="drag-over-gap-bottom")),e("li",{class:c()(n)},[this.switcher(),function(){var n=t.title,r=t.template;return e("span",{ref:"selectHandle",attrs:{class:"ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal draggable",draggable:t.draggable},on:{dragstart:t.onDragStart,dragenter:t.onDragEnter,dragover:t.onDragOver,dragleave:t.onDragLeave,drop:t.onDrop,dragend:t.onDragEnd}},[e(r,{attrs:{},props:{title:n,node:t.source}},[])])}(),this.renderChildren()])}});t.a=l},function(e,t){function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(t){return"function"==typeof Symbol&&"symbol"===n(Symbol.iterator)?e.exports=r=function(e){return n(e)}:e.exports=r=function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":n(e)},r(t)}e.exports=r},function(e,n){e.exports=t},function(e,t){e.exports="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNTA5NzY4OTY2MTY0IiBjbGFzcz0iaWNvbiIgc3R5bGU9IiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE4NTEiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjwvZGVmcz48cGF0aCBkPSJNNTQ2LjE0ODYxNiA2MjIuOTE3MTM5IDcwOC4yNTA0NzIgNDYwLjgxNTI4M0M3MTguNjIyOTYyIDQ1My4wMzE4NzIgNzI1LjMzMzMzMyA0NDAuNjMyOTE1IDcyNS4zMzMzMzMgNDI2LjY2NjY2NyA3MjUuMzMzMzMzIDQwMy4xMDI1MTcgNzA2LjIzMDgxNiAzODQgNjgyLjY2NjY2NyAzODRMMzQxLjMzMzMzMyAzODRDMzE3Ljc2OTE4NCAzODQgMjk4LjY2NjY2NyA0MDMuMTAyNTE3IDI5OC42NjY2NjcgNDI2LjY2NjY2NyAyOTguNjY2NjY3IDQ0MC42MzI5MTUgMzA1LjM3NzAzOSA0NTMuMDMxODcyIDMxNS43NDk1MjggNDYwLjgxNTI4M0w0NzcuODUxMzg0IDYyMi45MTcxMzlDNDg1LjYzNDc5NCA2MzMuMjg5NjI3IDQ5OC4wMzM3NTIgNjQwIDUxMiA2NDAgNTI1Ljk2NjI0OCA2NDAgNTM4LjM2NTIwNiA2MzMuMjg5NjI3IDU0Ni4xNDg2MTYgNjIyLjkxNzEzOUw1NDYuMTQ4NjE2IDYyMi45MTcxMzlaIiBwLWlkPSIxODUyIj48L3BhdGg+PC9zdmc+"},function(e,t,n){"use strict";var r=n(0),o=n.n(r);t.a=o.a.component("IVIEW_TEMPLATE",{props:{title:{},node:{type:Object}},render:function(){return(0,arguments[0])("span",{class:"ant-tree-title"},[this.title])}})},function(e,t,n){function r(e){for(var t=1;t=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var o=n(17);e.exports=r},function(e,t){function n(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}e.exports=n},function(e,t,n){var r=n(19);"string"==typeof r&&(r=[[e.i,r,""]]);var o={hmr:!0};o.transform=void 0;n(23)(r,o);r.locals&&(e.exports=r.locals)},function(e,t,n){var r=n(20);t=e.exports=n(21)(!1),t.push([e.i,"ul, li {\n margin: 0;\n padding: 0;\n}\nli {\n list-style: none;\n}\n.draggable {\n line-height: 14px;\n}\n\n.drag-over>.draggable {\n background: #2d8cf0;\n}\n.drag-over-gap-top>.draggable {\n border-top: 2px solid #2b85e4;\n}\n.drag-over-gap-bottom>.draggable {\n border-bottom: 2px solid #2b85e4;\n}\n\n.ivu-tree ul li {\n list-style: none;\n margin: 8px 0;\n padding: 0;\n white-space: nowrap;\n outline: 0;\n}\n.ivu-tree-arrow {\n cursor: pointer;\n width: 12px;\n text-align: center;\n display: inline-block;\n}\n.ivu-tree-arrow-open img {\n transform: rotate(-90deg);\n}\n.ivu-tree-arrow i {\n transition: all .2s ease-in-out;\n}\n\n.ivu-icon-arrow-right-b {\n background: url("+r(n(22))+");\n}\n/* title */\n.ivu-tree-title {\n display: inline-block;\n margin: 0;\n padding: 0 4px;\n border-radius: 3px;\n cursor: pointer;\n vertical-align: top;\n color: #495060;\n transition: all .2s ease-in-out;\n}\n.ivu-tree-title:hover {\n background-color: #eaf4fe;\n}\n.ivu-tree li ul {\n margin: 0;\n padding: 0 0 0 18px;\n}\n.ivu-tree li {\n white-space: nowrap;\n}\n.ivu-tree ul {\n list-style: none;\n margin: 0;\n padding: 0;\n /* font-size: 12px; */\n}\n.ivu-article li:not([class^=ivu-]) {\n margin-bottom: 5px;\n font-size: 14px;\n}\n\n/** ant design **/\n.ant-tree {\n margin: 0;\n padding: 0;\n font-size: 12px;\n}\n.ant-tree li {\n padding: 4px 0;\n margin: 0;\n list-style: none;\n white-space: nowrap;\n outline: 0;\n}\n.ant-tree li ul {\n margin: 0;\n padding: 0 0 0 18px;\n}\n\n.ant-tree li span[draggable=true], .ant-tree li span[draggable] {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n border-top: 2px solid transparent;\n border-bottom: 2px solid transparent;\n margin-top: -2px;\n -khtml-user-drag: element;\n -webkit-user-drag: element;\n}\n.ant-tree li .ant-tree-node-content-wrapper {\n display: inline-block;\n padding: 3px 5px;\n border-radius: 2px;\n margin: 0;\n cursor: pointer;\n text-decoration: none;\n vertical-align: top;\n color: rgba(0,0,0,.65);\n -webkit-transition: all .3s;\n transition: all .3s;\n position: relative;\n}\n.ant-tree-node-content-wrapper {\n width: calc(100% - 18px);\n}\n.ant-tree li .ant-tree-node-content-wrapper:hover {\n background-color: #ecf6fd;\n}\n.ant-tree li.drag-over-gap-top>span[draggable] {\n border-top-color: #108ee9;\n}\n.ant-tree li.drag-over-gap-bottom>span[draggable] {\n border-bottom-color: #108ee9;\n}\n\n",""])},function(e,t){e.exports=function(e){return"string"!=typeof e?e:(/^['"].*['"]$/.test(e)&&(e=e.slice(1,-1)),/["'() \t\n]/.test(e)?'"'+e.replace(/"/g,'\\"').replace(/\n/g,"\\n")+'"':e)}},function(e,t){function n(e,t){var n=e[1]||"",o=e[3];if(!o)return n;if(t&&"function"==typeof btoa){var i=r(o);return[n].concat(o.sources.map(function(e){return"/*# sourceURL="+o.sourceRoot+e+" */"})).concat([i]).join("\n")}return[n].join("\n")}function r(e){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(e))))+" */"}e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var r=n(t,e);return t[2]?"@media "+t[2]+"{"+r+"}":r}).join("")},t.i=function(e,n){"string"==typeof e&&(e=[[null,e,""]]);for(var r={},o=0;o=0&&M.splice(t,1)}function s(e){var t=document.createElement("style");return e.attrs.type="text/css",u(t,e.attrs),i(e,t),t}function c(e){var t=document.createElement("link");return e.attrs.type="text/css",e.attrs.rel="stylesheet",u(t,e.attrs),i(e,t),t}function u(e,t){Object.keys(t).forEach(function(n){e.setAttribute(n,t[n])})}function d(e,t){var n,r,o,i;if(t.transform&&e.css){if(!(i=t.transform(e.css)))return function(){};e.css=i}if(t.singleton){var u=N++;n=v||(v=s(t)),r=l.bind(null,n,u,!1),o=l.bind(null,n,u,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=c(t),r=f.bind(null,n,t),o=function(){a(n),n.href&&URL.revokeObjectURL(n.href)}):(n=s(t),r=p.bind(null,n),o=function(){a(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else o()}}function l(e,t,n,r){var o=n?"":r.css;if(e.styleSheet)e.styleSheet.cssText=m(t,o);else{var i=document.createTextNode(o),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(i,a[t]):e.appendChild(i)}}function p(e,t){var n=t.css,r=t.media;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}function f(e,t,n){var r=n.css,o=n.sourceMap,i=void 0===t.convertToAbsoluteUrls&&o;(t.convertToAbsoluteUrls||i)&&(r=b(r)),o&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */");var a=new Blob([r],{type:"text/css"}),s=e.href;e.href=URL.createObjectURL(a),s&&URL.revokeObjectURL(s)}var g={},h=function(e){var t;return function(){return void 0===t&&(t=e.apply(this,arguments)),t}}(function(){return window&&document&&document.all&&!window.atob}),y=function(e){var t={};return function(n){if(void 0===t[n]){var r=e.call(this,n);if(r instanceof window.HTMLIFrameElement)try{r=r.contentDocument.head}catch(e){r=null}t[n]=r}return t[n]}}(function(e){return document.querySelector(e)}),v=null,N=0,M=[],b=n(24);e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");t=t||{},t.attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||"boolean"==typeof t.singleton||(t.singleton=h()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=o(e,t);return r(n,t),function(e){for(var i=[],a=0;a 2 | 7 | 8 | 9 | 98 | 99 | 102 | -------------------------------------------------------------------------------- /examples/custom.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 253 | 254 | 257 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | rc-tree
-------------------------------------------------------------------------------- /examples/static/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/static/css/app.0df69f162c2b59b459c1f36f049df765.css: -------------------------------------------------------------------------------- 1 | li,ul{margin:0;padding:0}li{list-style:none}.draggable{line-height:14px}.drag-over>.draggable{background:#2d8cf0}.drag-over-gap-top>.draggable{border-top:2px solid #2b85e4}.drag-over-gap-bottom>.draggable{border-bottom:2px solid #2b85e4}.ivu-tree ul li{list-style:none;margin:8px 0;padding:0;white-space:nowrap;outline:0}.ivu-tree-arrow{cursor:pointer;width:12px;text-align:center;display:inline-block}.ivu-tree-arrow-open img{transform:rotate(-90deg)}.ivu-tree-arrow i{transition:all .2s ease-in-out}.ivu-icon-arrow-right-b{background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNTA5NzY4OTY2MTY0IiBjbGFzcz0iaWNvbiIgc3R5bGU9IiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE4NTEiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjwvZGVmcz48cGF0aCBkPSJNNTQ2LjE0ODYxNiA2MjIuOTE3MTM5IDcwOC4yNTA0NzIgNDYwLjgxNTI4M0M3MTguNjIyOTYyIDQ1My4wMzE4NzIgNzI1LjMzMzMzMyA0NDAuNjMyOTE1IDcyNS4zMzMzMzMgNDI2LjY2NjY2NyA3MjUuMzMzMzMzIDQwMy4xMDI1MTcgNzA2LjIzMDgxNiAzODQgNjgyLjY2NjY2NyAzODRMMzQxLjMzMzMzMyAzODRDMzE3Ljc2OTE4NCAzODQgMjk4LjY2NjY2NyA0MDMuMTAyNTE3IDI5OC42NjY2NjcgNDI2LjY2NjY2NyAyOTguNjY2NjY3IDQ0MC42MzI5MTUgMzA1LjM3NzAzOSA0NTMuMDMxODcyIDMxNS43NDk1MjggNDYwLjgxNTI4M0w0NzcuODUxMzg0IDYyMi45MTcxMzlDNDg1LjYzNDc5NCA2MzMuMjg5NjI3IDQ5OC4wMzM3NTIgNjQwIDUxMiA2NDAgNTI1Ljk2NjI0OCA2NDAgNTM4LjM2NTIwNiA2MzMuMjg5NjI3IDU0Ni4xNDg2MTYgNjIyLjkxNzEzOUw1NDYuMTQ4NjE2IDYyMi45MTcxMzlaIiBwLWlkPSIxODUyIj48L3BhdGg+PC9zdmc+)}.ivu-tree-title{display:inline-block;margin:0;padding:0 4px;border-radius:3px;cursor:pointer;vertical-align:top;color:#495060;transition:all .2s ease-in-out}.ivu-tree-title:hover{background-color:#eaf4fe}.ivu-tree li ul{margin:0;padding:0 0 0 18px}.ivu-tree li{white-space:nowrap}.ivu-tree ul{list-style:none;margin:0;padding:0}.ivu-article li:not([class^=ivu-]){margin-bottom:5px;font-size:14px}.ant-tree{margin:0;padding:0;font-size:12px}.ant-tree li{padding:4px 0;margin:0;list-style:none;white-space:nowrap;outline:0}.ant-tree li ul{margin:0;padding:0 0 0 18px}.ant-tree li span[draggable=true],.ant-tree li span[draggable]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-top:2px solid transparent;border-bottom:2px solid transparent;margin-top:-2px;-khtml-user-drag:element;-webkit-user-drag:element}.ant-tree li .ant-tree-node-content-wrapper{display:inline-block;padding:3px 5px;border-radius:2px;margin:0;cursor:pointer;text-decoration:none;vertical-align:top;color:rgba(0,0,0,.65);transition:all .3s;position:relative}.ant-tree-node-content-wrapper{width:calc(100% - 18px)}.ant-tree li .ant-tree-node-content-wrapper:hover{background-color:#ecf6fd}.ant-tree li.drag-over-gap-top>span[draggable]{border-top-color:#108ee9}.ant-tree li.drag-over-gap-bottom>span[draggable]{border-bottom-color:#108ee9} -------------------------------------------------------------------------------- /examples/static/js/app.c7898957126c69612825.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],{"42Hy":function(e,t,n){"use strict";function r(e){n("DtEM")}var o=n("zRxK"),i=n("87F3"),a=n("Mz/3"),d=r,c=a(o.a,i.a,!1,d,"data-v-2912994a",null);t.a=c.exports},"6Lwi":function(e,t){},"7xFW":function(e,t,n){"use strict";var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("pre",[e._v(" "),n("code",{staticClass:"nohighlight",domProps:{innerHTML:e._s(e.code)}},[e._v("\n "),e._t("default",[e._v("\n ")]),e._v("\n ")],2),e._v("\n ")])])},o=[],i={render:r,staticRenderFns:o};t.a=i},"87F3":function(e,t,n){"use strict";var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("div",{staticClass:"basic"},[n("h2",[e._v("基本用法")]),e._v(" "),n("p",[e._v("只是单纯渲染树")]),e._v(" "),n("div",[n("Basic")],1),e._v(" "),n("a",{attrs:{href:"https://github.com/ltaoo/vue-draggable-tree/blob/master/examples/basic.vue"}},[e._v("代码")])]),e._v(" "),n("div",{staticClass:"custom"},[n("h2",[e._v("自定义子节点内容")]),e._v(" "),n("p",[e._v("可以自定义内容,增加按钮、icon 等。\n ")]),n("div",[n("Custom")],1),e._v(" "),n("a",{attrs:{href:"https://github.com/ltaoo/vue-draggable-tree/blob/master/examples/custom.vue"}},[e._v("代码")])]),e._v(" "),n("div",[n("h2",[e._v("展示全部的 hook")]),e._v(" "),n("div",[n("Hooks")],1),e._v(" "),n("a",{attrs:{href:"https://github.com/ltaoo/vue-draggable-tree/blob/master/examples/hooks.vue"}},[e._v("代码")])])])},o=[],i={render:r,staticRenderFns:o};t.a=i},"9QQc":function(e,t,n){"use strict";function r(e){n("xPuw")}var o=n("u156"),i=n("7xFW"),a=n("Mz/3"),d=r,c=a(o.a,i.a,!1,d,null,null);t.a=c.exports},DtEM:function(e,t){},Fl5v:function(e,t,n){"use strict";function r(e){n("6Lwi")}var o=n("VOnD"),i=n("cVtn"),a=n("Mz/3"),d=r,c=a(o.a,i.a,!1,d,null,null);t.a=c.exports},GXbI:function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r={TOP:1,BOTTOM:-1,CONTENT:0}},Ilgj:function(e,t){},K9yv:function(e,t,n){"use strict";var r=n("wHeh");t.a=r.a.component("IVIEW_TEMPLATE",{props:{title:{},node:{type:Object}},render:function(){return(0,arguments[0])("span",{class:"ant-tree-title"},[this.title])}})},M93x:function(e,t,n){"use strict";function r(e){n("yCed")}var o=n("tfPZ"),i=n("UJPC"),a=n("Mz/3"),d=r,c=a(o.a,i.a,!1,d,null,null);t.a=c.exports},NHnr:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n("wHeh"),o=n("M93x");r.a.config.productionTip=!1,new r.a({el:"#app",template:"",components:{App:o.a}})},"Tu/+":function(e,t,n){"use strict";function r(e){n("Ilgj")}var o=n("qw+g"),i=n("kxDq"),a=n("Mz/3"),d=r,c=a(o.a,i.a,!1,d,null,null);t.a=c.exports},UJPC:function(e,t,n){"use strict";var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{attrs:{id:"app"}},[n("Index")],1)},o=[],i={render:r,staticRenderFns:o};t.a=i},VOnD:function(e,t,n){"use strict";var r=n("5+9Q"),o=n.n(r),i=n("wHeh"),a=n("cWM3"),d=[{key:"0",title:"女装",type:"clother",children:[{key:"0-1",title:"风衣",type:"clother"},{key:"0-2",title:"外套",type:"clother"}]},{key:"1",title:"男装",type:"clother"},{key:"2",title:"图书",type:"book",children:[{key:"2-0",title:"小说",type:"book",children:[{key:"2-0-0",title:"九州牧云录",type:"book"},{key:"2-0-1",title:"天空的城",type:"book"},{key:"2-0-2",title:"三体",type:"book"}]},{key:"2-1",title:"经管",type:"book",children:[{key:"2-1-0",title:"创京东",type:"book"}]},{key:"2-2",title:"科技",type:"book",children:[{key:"2-2-0",title:"JavaScript权威指南",highlight:!0,type:"book"},{key:"2-2-1",title:"JavaScript高级程序设计",type:"book"}]}]}];t.a={name:"vue-draggable-tree-demo",components:{Tree:a.c},data:function(){var e=(this.$createElement,this);return this.template=i.a.component("custom-tree-node",{props:["title","node"],render:function(){var t=arguments[0],n="margin-left: 10px; cursor: pointer;",r=t("span",{style:n,on:{click:e.addNode.bind(e,this.node)}},["新增"]),o=t("span",{style:n,on:{click:e.editNode.bind(e,this.node)}},["编辑"]),i=t("span",{style:n,on:{click:e.deleteNode.bind(e,this.node)}},["删除"]);return t("div",[t("span",{style:this.node.highlight?"color: red;":""},[this.title]),r,o,i])}}),{data:d}},methods:{afterInsert:function(){console.log(this.data)},addNode:function(e){Object(a.d)(this.data,e.key,function(e){e.children||i.a.set(e,"children",[]),e.children.push({key:Math.random(),title:"new node",children:[]})})},editNode:function(e){var t=window.prompt("请输入新标题");t&&Object(a.d)(this.data,e.key,function(e){e.title=t})},deleteNode:function(e){Object(a.d)(this.data,e.key,function(e,t,n){n.splice(t,1)})},handleDrop:function(e){var t=e.node,n=e.dragNode,r=e.targetPosition;console.log(t);var i=t.rckey,d=n.rckey;if(t.source.type!==n.source.type)return void alert("".concat(n.source.title," can't put in ").concat(t.source.type));var c=i,s=d,l=o()(this.data),u=Object(a.b)(l,s,c,r),h=u.targetSourceNode,g=u.targetSourceNodeIndex,p=u.targetSourceNodes,f=u.originSourceNode,v=u.originSourceNodeIndex,y=u.originSourceNodes;r===a.a.CONTENT&&(h.children=h.children||[],h.children.push(f),y.splice(v,1)),r===a.a.TOP&&Object(a.f)(g,p,f,v,y),r===a.a.BOTTOM&&Object(a.e)(g,p,f,v,y)}}}},W9xd:function(e,t,n){"use strict";var r=n("cWM3"),o=[{key:"0",title:"女装",children:[{key:"0-1",title:"风衣"},{key:"0-2",title:"外套"}]},{key:"1",title:"男装"},{key:"2",title:"图书",children:[{key:"2-0",title:"小说",children:[{key:"2-0-0",title:"九州牧云录"},{key:"2-0-1",title:"天空的城"},{key:"2-0-2",title:"三体"}]},{key:"2-1",title:"经管",children:[{key:"2-1-0",title:"创京东"}]},{key:"2-2",title:"科技",children:[{key:"2-2-0",title:"JavaScript权威指南"},{key:"2-2-1",title:"JavaScript高级程序设计"}]}]}];t.a={name:"vue-draggable-tree-demo",components:{Tree:r.c},data:function(){return{data:o}},methods:{afterInsert:function(){console.log(this.data)},handleDragEnd:function(e,t,n){console.log(e,t,n)}}}},YwDL:function(e,t,n){"use strict";var r=n("5+9Q"),o=n.n(r),i=n("wHeh"),a=n("hwOU"),d=n("K9yv"),c=n("GXbI"),s=n("hgXd"),l=n("rUkx");n.n(l);t.a=i.a.component("Tree",{props:{value:{type:Array},autoExpandParent:{type:Boolean,default:!0},draggable:{type:Boolean,default:!1},onDragEnd:{type:Function,default:s.h},onDragEnter:{type:Function,default:s.h},onDragLeave:{type:Function,default:s.h},onDragOver:{type:Function,default:s.h},onDragStart:{type:Function,default:s.h},onDrop:{type:Function},onExpand:{type:Function,default:s.h},template:{type:Function,default:d.a},beforeInner:{type:Function},beforeInsert:{type:Function},afterInsert:{type:Function}},data:function(){return this.draggingNodesKeys=[],{dragOverNodeKey:"",dropPosition:"",expandedKeys:[]}},computed:{data:function(){return this.value}},methods:{renderTreeNode:function(e){var t=e.key,n=e.title,r=e.pos,o=e.children,i=this.dragOverNodeKey===t&&this.dropPosition===c.a.TOP,d=this.dragOverNodeKey===t&&this.dropPosition===c.a.BOTTOM,s=this.dragOverNodeKey===t&&this.dropPosition===c.a.CONTENT,l=-1!==this.expandedKeys.indexOf(t);return this.$createElement(a.a,{props:{rckey:t,title:n,root:this,pos:r,children:o,dragOver:s,dragOverGapTop:i,dragOverGapBottom:d,template:this.template,draggable:this.draggable,expanded:l,source:e}})},handleStartDrag:function(e,t){this.draggingNode=t,this.draggingNodesKeys=Object(s.e)(t),this.onDragStart({event:e,node:t})},handleNodeEntered:function(e,t){var n=Object(s.a)(e,t);if(this.draggingNode.rckey===t.rckey&&0===n)return this.dragOverNodeKey="",void(this.dropPosition=null);this.dragOverNodeKey=t.rckey,this.dropPosition=n,this.onDragEnter({event:e,node:t})},handleNodeCrossed:function(e,t){this.onDragOver({event:e,node:t})},handleNodeLeaved:function(e,t){this.onDragLeave({event:e,node:t})},handleNodeDropped:function(e,t){var n=t.rckey,r=this.dropPosition;if(this.dragOverNodeKey="",this.draggingNodesKeys.includes(n))return void console.error("Can not drop to dragNode(include it's children node)");var i={event:e,node:t,dragNode:this.draggingNode,targetPosition:r};if(r!==c.a.CONTENT&&(i.dropToGap=!0),this.onDrop)return void this.onDrop(i);var a=n,d=this.draggingNode.rckey,l=o()(this.data),u=Object(s.b)(l,d,a,r),h=u.targetSourceNode,g=u.targetSourceNodeIndex,p=u.targetSourceNodes,f=u.originSourceNode,v=u.originSourceNodeIndex,y=u.originSourceNodes;if(r===c.a.CONTENT){if(this.beforeInner)return void this.beforeInner("inner",h.children,f);h.children=h.children||[],h.children.push(f),y.splice(v,1)}if(this.beforeInsert)return void this.beforeInsert("insert",p,g,f);r===c.a.TOP&&Object(s.g)(g,p,f,v,y),r===c.a.BOTTOM&&Object(s.f)(g,p,f,v,y),this.$emit("input",l),this.afterInsert&&this.afterInsert()},handleDragEnd:function(e,t){this.dragOverNodeKey="",this.$emit("dragEnd",this.data,t,e)},expand:function(e){var t=!e.expanded,n=o()(this.expandedKeys),r=e.rckey,i=n.indexOf(r);t&&-1===i?n.push(r):!t&&i>-1&&n.splice(i,1),this.expandedKeys=n,this.onExpand(n,{node:e,expanded:t})}},render:function(){return(0,arguments[0])("ul",{class:"ant-tree tree",attrs:{role:"tree-node",unselectable:"on"}},[Object(s.d)(this.data).map(this.renderTreeNode)])}})},a82o:function(e,t,n){"use strict";var r=function(){var e=this,t=e.$createElement;return(e._self._c||t)("Tree",{attrs:{draggable:""},model:{value:e.data,callback:function(t){e.data=t},expression:"data"}})},o=[],i={render:r,staticRenderFns:o};t.a=i},cVtn:function(e,t,n){"use strict";var r=function(){var e=this,t=e.$createElement;return(e._self._c||t)("Tree",{attrs:{draggable:"",afterInsert:e.afterInsert,template:e.template,onDrop:e.handleDrop},model:{value:e.data,callback:function(t){e.data=t},expression:"data"}})},o=[],i={render:r,staticRenderFns:o};t.a=i},cWM3:function(e,t,n){"use strict";var r=n("YwDL"),o=n("hgXd");n.d(t,"b",function(){return o.b}),n.d(t,"d",function(){return o.c}),n.d(t,"e",function(){return o.f}),n.d(t,"f",function(){return o.g});var i=n("GXbI");n.d(t,"a",function(){return i.a}),t.c=r.a},d5if:function(e,t){e.exports="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNTA5NzY4OTY2MTY0IiBjbGFzcz0iaWNvbiIgc3R5bGU9IiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE4NTEiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48L3N0eWxlPjwvZGVmcz48cGF0aCBkPSJNNTQ2LjE0ODYxNiA2MjIuOTE3MTM5IDcwOC4yNTA0NzIgNDYwLjgxNTI4M0M3MTguNjIyOTYyIDQ1My4wMzE4NzIgNzI1LjMzMzMzMyA0NDAuNjMyOTE1IDcyNS4zMzMzMzMgNDI2LjY2NjY2NyA3MjUuMzMzMzMzIDQwMy4xMDI1MTcgNzA2LjIzMDgxNiAzODQgNjgyLjY2NjY2NyAzODRMMzQxLjMzMzMzMyAzODRDMzE3Ljc2OTE4NCAzODQgMjk4LjY2NjY2NyA0MDMuMTAyNTE3IDI5OC42NjY2NjcgNDI2LjY2NjY2NyAyOTguNjY2NjY3IDQ0MC42MzI5MTUgMzA1LjM3NzAzOSA0NTMuMDMxODcyIDMxNS43NDk1MjggNDYwLjgxNTI4M0w0NzcuODUxMzg0IDYyMi45MTcxMzlDNDg1LjYzNDc5NCA2MzMuMjg5NjI3IDQ5OC4wMzM3NTIgNjQwIDUxMiA2NDAgNTI1Ljk2NjI0OCA2NDAgNTM4LjM2NTIwNiA2MzMuMjg5NjI3IDU0Ni4xNDg2MTYgNjIyLjkxNzEzOUw1NDYuMTQ4NjE2IDYyMi45MTcxMzlaIiBwLWlkPSIxODUyIj48L3BhdGg+PC9zdmc+"},hgXd:function(e,t,n){"use strict";function r(){}function o(){function e(t,r,o,i){var a=t;t&&t.length&&(a=t.filter(Boolean)),a.forEach(function(t,r){if(t.isTreeNode){var a=t.pos;o.push(a);var d=[];t.$children&&e(t.$children,a,d,a),n(t,r,a,t.rckey||a,d,i)}})}var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],n=arguments.length>1?arguments[1]:void 0;e(t,0,[])}function i(e,t){return e.every(function(e,n){return e===t[n]})}function a(e){var t=[],n=e.pos.split("-");return o(e.$children,function(r,o,a,d){var c=a.split("-");(e.pos===a||n.lengthr+o-2?v.a.BOTTOM:ie?(o.splice(r,1),t.splice(e,0,n),{targetSourceNodes:t,originSourceNodes:o}):(t.splice(e,0,n),o.splice(r,1),{targetSourceNodes:t,originSourceNodes:o})}function u(e,t,n,r,o){var i=e+1;return o===t&&e>r&&(i=e),o.splice(r,1),t.splice(i,0,n),{targetSourceNodes:t,originSourceNodes:o}}t.h=r,n.d(t,"d",function(){return y}),t.e=a,t.a=c,n.d(t,"c",function(){return N}),t.b=s,t.g=l,t.f=u;var h=n("MzIN"),g=n.n(h),p=n("Uelc"),f=n.n(p),v=n("GXbI"),y=function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,r=arguments.length>2?arguments[2]:void 0;return t.map(function(t,o){var i=t.key,a=t.title,d=f()(t,["key","title"]),c=g()({},d,{key:i,title:a,pos:void 0===r?String(o):"".concat(r,"-").concat(o)});if(t.children&&t.children.length){var s=n+1;c.children=e(t.children,s,c.pos)}return c})},N=function e(t,n,r){t.forEach(function(t,o,i){return t.key===n?r(t,o,i):!!t.children&&e(t.children,n,r)})}},hwOU:function(e,t,n){"use strict";var r=n("N8BZ"),o=n.n(r),i=n("wHeh"),a=n("lIaQ"),d=n.n(a),c=n("d5if"),s=n.n(c),l=i.a.component("TreeNode",{props:{prefixCls:{type:String,default:function(){return"rc"}},source:{type:Object},title:{type:String,default:function(){return"---"}},rckey:{},pos:{type:String},root:{type:Object},children:{type:Array},draggable:{type:Boolean,default:!1},dragOver:{},dragOverGapTop:{type:Boolean},dragOverGapBottom:{type:Boolean},template:{},expanded:{type:Boolean}},data:function(){return this.isTreeNode=!0,{dataLoading:!1,dragNodeHighlight:!1}},computed:{handleSelect:function(){return this.$refs.handleSelect}},methods:{renderChildren:function(){var e=this,t=this.$createElement,n=this.expanded,r=this.children,o=null;return r&&!n&&(o=t("ul",{class:"ivu-tree-children"},[r.map(function(t){return e.root.renderTreeNode(t)})])),o},onSelect:function(){this.root.onSelect(this)},onDragStart:function(e){e.stopPropagation(),this.dragNodeHighlight=!0,this.root.handleStartDrag(e,this);try{e.dataTransfer.setData("text/plain","")}catch(e){}},onDragEnter:function(e){console.log(this.title,"drag enter",e.target),e.preventDefault(),e.stopPropagation(),this.root.handleNodeEntered(e,this)},onDragOver:function(e){e.preventDefault(),e.stopPropagation(),this.root.handleNodeCrossed(e,this)},onDragLeave:function(e){console.log(this.title,"drag leave",e.target),e.stopPropagation(),this.root.handleNodeLeaved(e,this)},onDrop:function(e){e.preventDefault(),e.stopPropagation(),this.dragNodeHighlight=!1,this.root.handleNodeDropped(e,this)},onDragEnd:function(e){e.stopPropagation(),this.dragNodeHighlight=!1,this.root.handleDragEnd(e,this)},onExpand:function(){var e=this,t=this.root.expand(this);if(t&&"object"===o()(t)){var n=function(t){e.dataLoading=t};n(!0),t.then(function(){n(!1)},function(){n(!1)})}},switcher:function(){var e=this.$createElement,t="";return this.children&&this.children.length>0&&this.expanded&&(t+=" ivu-tree-arrow-open"),e("span",{on:{click:this.onExpand},class:"ivu-tree-arrow".concat(t)},[this.children&&this.children.length?e("img",{style:"width: 16px; vertical-align: bottom;",attrs:{src:s.a}}):null])}},render:function(e){var t=this,n="";return this.disabled||(this.dragOver?n="drag-over":this.dragOverGapTop?n="drag-over-gap-top":this.dragOverGapBottom&&(n="drag-over-gap-bottom")),e("li",{class:d()(n)},[this.switcher(),function(){var n=t.title,r=t.template;return e("span",{ref:"selectHandle",attrs:{class:"ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal draggable",draggable:t.draggable},on:{dragstart:t.onDragStart,dragenter:t.onDragEnter,dragover:t.onDragOver,dragleave:t.onDragLeave,drop:t.onDrop,dragend:t.onDragEnd}},[e(r,{attrs:{},props:{title:n,node:t.source}},[])])}(),this.renderChildren()])}});t.a=l},kxDq:function(e,t,n){"use strict";var r=function(){var e=this,t=e.$createElement;return(e._self._c||t)("Tree",{attrs:{draggable:"",afterInsert:e.afterInsert,template:e.template,onDragStart:e.handleDragStart,onDragEnter:e.handleDragEnter,onDragLeave:e.handleDragLeave,onDrop:e.handleDrop},on:{dragEnd:e.handleDragEnd},model:{value:e.data,callback:function(t){e.data=t},expression:"data"}})},o=[],i={render:r,staticRenderFns:o};t.a=i},laN3:function(e,t,n){"use strict";function r(e){n("nWsl")}var o=n("W9xd"),i=n("a82o"),a=n("Mz/3"),d=r,c=a(o.a,i.a,!1,d,null,null);t.a=c.exports},nWsl:function(e,t){},"qw+g":function(e,t,n){"use strict";var r=n("5+9Q"),o=n.n(r),i=n("wHeh"),a=n("cWM3"),d=[{key:"0",title:"女装",type:"clother",children:[{key:"0-1",title:"风衣",type:"clother"},{key:"0-2",title:"外套",type:"clother"}]},{key:"1",title:"男装",type:"clother"},{key:"2",title:"图书",type:"book",children:[{key:"2-0",title:"小说",type:"book",children:[{key:"2-0-0",title:"九州牧云录",type:"book"},{key:"2-0-1",title:"天空的城",type:"book"},{key:"2-0-2",title:"三体",type:"book"}]},{key:"2-1",title:"经管",type:"book",children:[{key:"2-1-0",title:"创京东",type:"book"}]},{key:"2-2",title:"科技",type:"book",children:[{key:"2-2-0",title:"JavaScript权威指南",highlight:!0,type:"book"},{key:"2-2-1",title:"JavaScript高级程序设计",type:"book"}]}]}];t.a={name:"vue-draggable-tree-demo",components:{Tree:a.c},data:function(){var e=(this.$createElement,this);return this.template=i.a.component("custom-tree-node",{props:["title","node"],render:function(){var t=arguments[0],n="margin-left: 10px; cursor: pointer;",r=t("span",{style:n,on:{click:e.addNode.bind(e,this.node)}},["新增"]),o=t("span",{style:n,on:{click:e.editNode.bind(e,this.node)}},["编辑"]),i=t("span",{style:n,on:{click:e.deleteNode.bind(e,this.node)}},["删除"]);return t("div",[t("span",{style:this.node.highlight?"color: red;":""},[this.title]),r,o,i])}}),{data:d}},methods:{afterInsert:function(){console.log(this.data)},addNode:function(e){Object(a.d)(this.data,e.key,function(e){e.children||i.a.set(e,"children",[]),e.children.push({key:Math.random(),title:"new node",children:[]})})},editNode:function(e){var t=window.prompt("请输入新标题");t&&Object(a.d)(this.data,e.key,function(e){e.title=t})},deleteNode:function(e){Object(a.d)(this.data,e.key,function(e,t,n){n.splice(t,1)})},handleDragStart:function(e){var t=e.node;console.log("start drag",t.title)},handleDragEnter:function(e){e.node.$refs.selectHandle.style.background="red"},handleDragLeave:function(e){var t=e.node;console.log("leave",t.$refs.selectHandle),t.$refs.selectHandle.style.background="unset"},handleDrop:function(e){var t=e.node,n=e.dragNode,r=e.targetPosition;console.log(t);var i=t.rckey,d=n.rckey;if(t.source.type!==n.source.type)return void alert("".concat(n.source.title," can't put in ").concat(t.source.type));var c=i,s=d,l=o()(this.data),u=Object(a.b)(l,s,c,r),h=u.targetSourceNode,g=u.targetSourceNodeIndex,p=u.targetSourceNodes,f=u.originSourceNode,v=u.originSourceNodeIndex,y=u.originSourceNodes;r===a.a.CONTENT&&(h.children=h.children||[],h.children.push(f),y.splice(v,1)),r===a.a.TOP&&Object(a.f)(g,p,f,v,y),r===a.a.BOTTOM&&Object(a.e)(g,p,f,v,y)},handleDragEnd:function(){alert("invoke after drop")}}}},rUkx:function(e,t){},tfPZ:function(e,t,n){"use strict";var r=n("42Hy");t.a={name:"app",components:{Index:r.a}}},u156:function(e,t,n){"use strict";t.a={name:"high-light",props:["code"]}},xPuw:function(e,t){},yCed:function(e,t){},zRxK:function(e,t,n){"use strict";var r=n("9QQc"),o=n("laN3"),i=n("Fl5v"),a=n("Tu/+");t.a={name:"vue-draggable-tree-demo",components:{Highlight:r.a,Basic:o.a,Custom:i.a,Hooks:a.a},data:function(){return{name:"demo"}}}}},["NHnr"]); 2 | //# sourceMappingURL=app.c7898957126c69612825.js.map -------------------------------------------------------------------------------- /examples/static/js/manifest.7b5e097f4d45823be1ad.js: -------------------------------------------------------------------------------- 1 | !function(r){function n(e){if(o[e])return o[e].exports;var t=o[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,n),t.l=!0,t.exports}var e=window.webpackJsonp;window.webpackJsonp=function(o,u,c){for(var f,i,p,a=0,l=[];a 2 | 3 | 4 | 5 | rc-tree 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-draggable-tree", 3 | "version": "1.1.6", 4 | "description": "draggable tree", 5 | "author": "ltaoo ", 6 | "scripts": { 7 | "dev": "node build/dev-server.js", 8 | "start": "npm run dev", 9 | "build": "node build/build.js", 10 | "dist": "webpack" 11 | }, 12 | "main": "dist/vue-draggable-tree.min.js", 13 | "entry": { 14 | "rc-tree": [ 15 | "./src/index.js" 16 | ] 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ltaoo/vue-draggable-tree.git" 21 | }, 22 | "homepage": "https://github.com/ltaoo/vue-draggable-tree/", 23 | "bugs": { 24 | "url": "https://github.com/ltaoo/vue-draggable-tree/issues" 25 | }, 26 | "dependencies": { 27 | "@babel/runtime": "^7.4.5", 28 | "classnames": "^2.2.5", 29 | "vue": "^2.5.2" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.4.5", 33 | "@babel/plugin-syntax-jsx": "^7.2.0", 34 | "@babel/plugin-transform-runtime": "^7.4.4", 35 | "@babel/preset-env": "^7.4.5", 36 | "autoprefixer": "^7.1.2", 37 | "babel-eslint": "^8.2.6", 38 | "babel-helper-vue-jsx-merge-props": "^2.0.2", 39 | "babel-jest": "^24.8.0", 40 | "babel-loader": "^8.0.6", 41 | "babel-plugin-syntax-jsx": "^6.18.0", 42 | "babel-plugin-transform-vue-jsx": "^3.7.0", 43 | "chalk": "^2.0.1", 44 | "connect-history-api-fallback": "^1.3.0", 45 | "copy-webpack-plugin": "^4.0.1", 46 | "css-loader": "^0.28.7", 47 | "eslint": "^4.18.2", 48 | "eslint-config-airbnb-base": "^11.3.2", 49 | "eslint-friendly-formatter": "^3.0.0", 50 | "eslint-import-resolver-webpack": "^0.8.3", 51 | "eslint-loader": "^1.7.1", 52 | "eslint-plugin-html": "^3.0.0", 53 | "eslint-plugin-import": "^2.7.0", 54 | "eventsource-polyfill": "^0.9.6", 55 | "express": "^4.14.1", 56 | "extract-text-webpack-plugin": "^3.0.0", 57 | "file-loader": "^1.1.4", 58 | "friendly-errors-webpack-plugin": "^1.6.1", 59 | "html-webpack-plugin": "^2.30.1", 60 | "http-proxy-middleware": "^0.17.3", 61 | "jest": "^24.8.0", 62 | "opn": "^5.1.0", 63 | "optimize-css-assets-webpack-plugin": "^3.2.0", 64 | "ora": "^1.2.0", 65 | "portfinder": "^1.0.13", 66 | "rimraf": "^2.6.0", 67 | "semver": "^5.3.0", 68 | "shelljs": "^0.7.6", 69 | "style-loader": "^0.19.0", 70 | "url-loader": "^0.5.8", 71 | "vue-loader": "^13.3.0", 72 | "vue-style-loader": "^3.0.1", 73 | "vue-template-compiler": "^2.5.2", 74 | "webpack": "^3.6.0", 75 | "webpack-bundle-analyzer": "^3.3.2", 76 | "webpack-dev-middleware": "^1.12.0", 77 | "webpack-hot-middleware": "^2.18.2", 78 | "webpack-merge": "^4.1.0" 79 | }, 80 | "engines": { 81 | "node": ">= 4.0.0", 82 | "npm": ">= 3.0.0" 83 | }, 84 | "browserslist": [ 85 | "> 1%", 86 | "last 2 versions", 87 | "not ie <= 8" 88 | ], 89 | "jest": { 90 | "moduleNameMapper": { 91 | "^@/(.*)$": "/src/$1" 92 | }, 93 | "transform": { 94 | "^.+\\.js$": "/node_modules/babel-jest" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /src/assets/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltaoo/vue-draggable-tree/bce93d1013125ade104352b1d9b739a53edbe599/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Highlight.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/components/Index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 51 | 52 | 53 | 55 | -------------------------------------------------------------------------------- /src/components/basic.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 100 | 101 | 104 | -------------------------------------------------------------------------------- /src/components/custom.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 245 | 246 | 249 | -------------------------------------------------------------------------------- /src/components/hooks.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 263 | 264 | 267 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import App from './App'; 5 | 6 | Vue.config.productionTip = false; 7 | 8 | /* eslint-disable no-new */ 9 | new Vue({ 10 | el: '#app', 11 | template: '', 12 | components: { App }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/test/customData.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: '0', 4 | title: '女装', 5 | children: [ 6 | { 7 | key: '0-1', 8 | title: '风衣', 9 | type: 'clother', 10 | }, 11 | { 12 | key: '0-2', 13 | title: '外套', 14 | type: 'clother', 15 | }, 16 | ], 17 | }, 18 | { 19 | key: '1', 20 | title: '男装', 21 | type: 'clother', 22 | }, 23 | { 24 | key: '2', 25 | title: '图书', 26 | type: 'book', 27 | children: [ 28 | { 29 | key: '2-0', 30 | title: '小说', 31 | type: 'book', 32 | children: [ 33 | { 34 | key: '2-0-0', 35 | title: '九州牧云录', 36 | type: 'book', 37 | }, 38 | { 39 | key: '2-0-1', 40 | title: '天空的城', 41 | type: 'book', 42 | }, 43 | { 44 | key: '2-0-2', 45 | title: '三体', 46 | type: 'book', 47 | }, 48 | ], 49 | }, 50 | { 51 | key: '2-1', 52 | title: '经管', 53 | type: 'book', 54 | children: [ 55 | { 56 | key: '2-1-0', 57 | title: '创京东', 58 | type: 'book', 59 | }, 60 | ], 61 | }, 62 | { 63 | key: '2-2', 64 | title: '科技', 65 | type: 'book', 66 | children: [ 67 | { 68 | key: '2-2-0', 69 | title: 'JavaScript权威指南', 70 | highlight: true, 71 | type: 'book', 72 | }, 73 | { 74 | key: '2-2-1', 75 | title: 'JavaScript高级程序设计', 76 | type: 'book', 77 | }, 78 | ], 79 | }, 80 | ], 81 | }, 82 | ]; 83 | -------------------------------------------------------------------------------- /src/test/utils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | traverseTreeNodes, 3 | formatSourceNodes, 4 | getDraggingNodesKey, 5 | findSourceNodeByKey, 6 | computeMoveNeededParams, 7 | insertToBottom, 8 | insertToTop, 9 | } from '../tree/utils'; 10 | 11 | import customSourceNodes from './customData'; 12 | import { TARGET_POSITION_TYPE } from '../tree/constants'; 13 | 14 | describe('util function', () => { 15 | it('add extra props to source node', () => { 16 | const sourceNodes = [ 17 | { 18 | key: 0, 19 | title: '图书', 20 | children: [ 21 | { 22 | key: 10, 23 | title: '科技', 24 | children: [ 25 | { 26 | key: 101, 27 | title: 'JavaScript权威指南', 28 | }, 29 | { 30 | key: 102, 31 | title: 'JavaScript高级程序设计', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | { 38 | key: 2, 39 | title: '服装', 40 | }, 41 | ]; 42 | 43 | const res = formatSourceNodes(sourceNodes); 44 | 45 | expect(res).toEqual([ 46 | { 47 | key: 0, 48 | pos: '0', 49 | title: '图书', 50 | children: [ 51 | { 52 | key: 10, 53 | pos: '0-0', 54 | title: '科技', 55 | children: [ 56 | { 57 | key: 101, 58 | pos: '0-0-0', 59 | title: 'JavaScript权威指南', 60 | }, 61 | { 62 | key: 102, 63 | pos: '0-0-1', 64 | title: 'JavaScript高级程序设计', 65 | }, 66 | ], 67 | }, 68 | ], 69 | }, 70 | { 71 | key: 2, 72 | pos: '1', 73 | title: '服装', 74 | }, 75 | ]); 76 | }); 77 | 78 | it('has some spec props', () => { 79 | const res = formatSourceNodes(customSourceNodes); 80 | 81 | expect(res).toEqual([ 82 | { 83 | key: '0', 84 | pos: '0', 85 | title: '女装', 86 | children: [ 87 | { 88 | key: '0-1', 89 | pos: '0-0', 90 | title: '风衣', 91 | type: 'clother', 92 | }, 93 | { 94 | key: '0-2', 95 | pos: '0-1', 96 | title: '外套', 97 | type: 'clother', 98 | }, 99 | ], 100 | }, 101 | { 102 | key: '1', 103 | pos: '1', 104 | title: '男装', 105 | type: 'clother', 106 | }, 107 | { 108 | key: '2', 109 | pos: '2', 110 | title: '图书', 111 | type: 'book', 112 | children: [ 113 | { 114 | key: '2-0', 115 | pos: '2-0', 116 | title: '小说', 117 | type: 'book', 118 | children: [ 119 | { 120 | key: '2-0-0', 121 | pos: '2-0-0', 122 | title: '九州牧云录', 123 | type: 'book', 124 | }, 125 | { 126 | key: '2-0-1', 127 | pos: '2-0-1', 128 | title: '天空的城', 129 | type: 'book', 130 | }, 131 | { 132 | key: '2-0-2', 133 | pos: '2-0-2', 134 | title: '三体', 135 | type: 'book', 136 | }, 137 | ], 138 | }, 139 | { 140 | key: '2-1', 141 | pos: '2-1', 142 | title: '经管', 143 | type: 'book', 144 | children: [ 145 | { 146 | key: '2-1-0', 147 | pos: '2-1-0', 148 | title: '创京东', 149 | type: 'book', 150 | }, 151 | ], 152 | }, 153 | { 154 | key: '2-2', 155 | pos: '2-2', 156 | title: '科技', 157 | type: 'book', 158 | children: [ 159 | { 160 | key: '2-2-0', 161 | pos: '2-2-0', 162 | title: 'JavaScript权威指南', 163 | highlight: true, 164 | type: 'book', 165 | }, 166 | { 167 | key: '2-2-1', 168 | pos: '2-2-1', 169 | title: 'JavaScript高级程序设计', 170 | type: 'book', 171 | }, 172 | ], 173 | }, 174 | ], 175 | }, 176 | ]); 177 | }); 178 | 179 | it('traverse tree nodes', () => { 180 | const mockTreeNode = { 181 | rckey: '2-2', 182 | pos: '0-0-0', 183 | isTreeNode: true, 184 | title: '科技', 185 | $children: [ 186 | { 187 | rckey: '2-2-0', 188 | isTreeNode: true, 189 | pos: '0-0-0-0', 190 | title: 'JavaScript权威指南', 191 | }, 192 | { 193 | rckey: '2-2-1', 194 | isTreeNode: true, 195 | pos: '0-0-0-1', 196 | title: 'JavaScript高级程序设计', 197 | }, 198 | ], 199 | }; 200 | const callback = jest.fn(); 201 | 202 | traverseTreeNodes(mockTreeNode.$children, callback); 203 | 204 | expect(callback.mock.calls.length).toBe(2); 205 | // index 206 | expect(callback.mock.calls[0][1]).toBe(0); 207 | expect(callback.mock.calls[0][2]).toBe('0-0-0-0'); 208 | // key 209 | expect(callback.mock.calls[0][3]).toBe('2-2-0'); 210 | 211 | expect(callback.mock.calls[1][1]).toBe(1); 212 | expect(callback.mock.calls[1][2]).toBe('0-0-0-1'); 213 | expect(callback.mock.calls[1][3]).toBe('2-2-1'); 214 | }); 215 | 216 | it('get dragging nodes key', () => { 217 | const mockTreeNode = { 218 | rckey: '2-2', 219 | pos: '0-0-0', 220 | isTreeNode: true, 221 | title: '科技', 222 | $children: [ 223 | { 224 | rckey: '2-2-0', 225 | isTreeNode: true, 226 | pos: '0-0-0-0', 227 | title: 'JavaScript权威指南', 228 | }, 229 | { 230 | rckey: '2-2-1', 231 | isTreeNode: true, 232 | pos: '0-0-0-1', 233 | title: 'JavaScript高级程序设计', 234 | }, 235 | ], 236 | }; 237 | 238 | const res = getDraggingNodesKey(mockTreeNode); 239 | 240 | expect(res).toEqual(['2-2-0', '2-2-1', '2-2']); 241 | }); 242 | 243 | it('find source node by key', () => { 244 | const sourceNodes = [ 245 | { 246 | key: 0, 247 | title: '图书', 248 | children: [ 249 | { 250 | key: 10, 251 | title: '科技', 252 | children: [ 253 | { 254 | key: 101, 255 | title: 'JavaScript权威指南', 256 | }, 257 | { 258 | key: 102, 259 | title: 'JavaScript高级程序设计', 260 | }, 261 | ], 262 | }, 263 | ], 264 | }, 265 | { 266 | key: 2, 267 | title: '服装', 268 | }, 269 | ]; 270 | const callback = jest.fn(); 271 | 272 | findSourceNodeByKey(sourceNodes, 101, callback); 273 | 274 | expect(callback.mock.calls.length).toBe(1); 275 | expect(callback.mock.calls[0][0]).toMatchObject({ 276 | key: 101, 277 | title: 'JavaScript权威指南', 278 | }); 279 | expect(callback.mock.calls[0][1]).toBe(0); 280 | expect(callback.mock.calls[0][2]).toMatchObject([ 281 | { 282 | key: 101, 283 | title: 'JavaScript权威指南', 284 | }, 285 | { 286 | key: 102, 287 | title: 'JavaScript高级程序设计', 288 | }, 289 | ]); 290 | }); 291 | 292 | describe('compute target action and need to operated source nodes', () => { 293 | const sourceNodes = [ 294 | { 295 | key: 0, 296 | title: '图书', 297 | children: [ 298 | { 299 | key: 10, 300 | title: '科技', 301 | children: [ 302 | { 303 | key: 101, 304 | title: 'JavaScript权威指南', 305 | }, 306 | { 307 | key: 102, 308 | title: 'JavaScript高级程序设计', 309 | }, 310 | ], 311 | }, 312 | ], 313 | }, 314 | { 315 | key: 2, 316 | title: '服装', 317 | }, 318 | ]; 319 | it('inner', () => { 320 | const res = computeMoveNeededParams( 321 | sourceNodes, 322 | 102, 323 | 0, 324 | TARGET_POSITION_TYPE.CONTENT, 325 | ); 326 | expect(res).toEqual({ 327 | targetSourceNode: { 328 | key: 0, 329 | title: '图书', 330 | children: [ 331 | { 332 | key: 10, 333 | title: '科技', 334 | children: [ 335 | { 336 | key: 101, 337 | title: 'JavaScript权威指南', 338 | }, 339 | { 340 | key: 102, 341 | title: 'JavaScript高级程序设计', 342 | }, 343 | ], 344 | }, 345 | ], 346 | }, 347 | originSourceNode: { 348 | key: 102, 349 | title: 'JavaScript高级程序设计', 350 | }, 351 | originSourceNodeIndex: 1, 352 | originSourceNodes: [ 353 | { 354 | key: 101, 355 | title: 'JavaScript权威指南', 356 | }, 357 | { 358 | key: 102, 359 | title: 'JavaScript高级程序设计', 360 | }, 361 | ], 362 | targetSourceNodes: undefined, 363 | targetSourceNodeIndex: undefined, 364 | }); 365 | }); 366 | 367 | it('move to top', () => { 368 | const res = computeMoveNeededParams( 369 | sourceNodes, 370 | 102, 371 | 0, 372 | TARGET_POSITION_TYPE.TOP, 373 | ); 374 | expect(res).toEqual({ 375 | targetSourceNodes: [ 376 | { 377 | key: 0, 378 | title: '图书', 379 | children: [ 380 | { 381 | key: 10, 382 | title: '科技', 383 | children: [ 384 | { 385 | key: 101, 386 | title: 'JavaScript权威指南', 387 | }, 388 | { 389 | key: 102, 390 | title: 'JavaScript高级程序设计', 391 | }, 392 | ], 393 | }, 394 | ], 395 | }, 396 | { 397 | key: 2, 398 | title: '服装', 399 | }, 400 | ], 401 | targetSourceNodeIndex: 0, 402 | originSourceNode: { 403 | key: 102, 404 | title: 'JavaScript高级程序设计', 405 | }, 406 | originSourceNodeIndex: 1, 407 | originSourceNodes: [ 408 | { 409 | key: 101, 410 | title: 'JavaScript权威指南', 411 | }, 412 | { 413 | key: 102, 414 | title: 'JavaScript高级程序设计', 415 | }, 416 | ], 417 | }); 418 | }); 419 | 420 | it('move to bottom', () => { 421 | const res = computeMoveNeededParams( 422 | sourceNodes, 423 | 102, 424 | 0, 425 | TARGET_POSITION_TYPE.TOP, 426 | ); 427 | expect(res).toEqual({ 428 | targetSourceNodes: [ 429 | { 430 | key: 0, 431 | title: '图书', 432 | children: [ 433 | { 434 | key: 10, 435 | title: '科技', 436 | children: [ 437 | { 438 | key: 101, 439 | title: 'JavaScript权威指南', 440 | }, 441 | { 442 | key: 102, 443 | title: 'JavaScript高级程序设计', 444 | }, 445 | ], 446 | }, 447 | ], 448 | }, 449 | { 450 | key: 2, 451 | title: '服装', 452 | }, 453 | ], 454 | targetSourceNodeIndex: 0, 455 | originSourceNode: { 456 | key: 102, 457 | title: 'JavaScript高级程序设计', 458 | }, 459 | originSourceNodeIndex: 1, 460 | originSourceNodes: [ 461 | { 462 | key: 101, 463 | title: 'JavaScript权威指南', 464 | }, 465 | { 466 | key: 102, 467 | title: 'JavaScript高级程序设计', 468 | }, 469 | ], 470 | }); 471 | }); 472 | }); 473 | 474 | describe('sample case of move to top', () => { 475 | it('case 1, same source and move to leftmost', () => { 476 | const targetSourceNodeIndex = 0; 477 | const targetSourceNodes = ['a', 'b', 'c', 'd']; 478 | const originSourceNode = 'd'; 479 | const originSourceNodeIndex = 3; 480 | 481 | const res = insertToTop( 482 | targetSourceNodeIndex, 483 | targetSourceNodes, 484 | originSourceNode, 485 | originSourceNodeIndex, 486 | targetSourceNodes, 487 | ); 488 | 489 | expect(res.targetSourceNodes).toEqual(['d', 'a', 'b', 'c']); 490 | }); 491 | it('case 2, same source and rightmost', () => { 492 | const targetSourceNodeIndex = 3; 493 | const targetSourceNodes = ['a', 'b', 'c', 'd']; 494 | const originSourceNode = 'b'; 495 | const originSourceNodeIndex = 1; 496 | 497 | const res = insertToTop( 498 | targetSourceNodeIndex, 499 | targetSourceNodes, 500 | originSourceNode, 501 | originSourceNodeIndex, 502 | targetSourceNodes, 503 | ); 504 | 505 | expect(res.targetSourceNodes).toEqual(['a', 'c', 'b', 'd']); 506 | }); 507 | it('case 3, same source and middle', () => { 508 | const targetSourceNodeIndex = 2; 509 | const targetSourceNodes = ['a', 'b', 'c', 'd']; 510 | const originSourceNode = 'a'; 511 | const originSourceNodeIndex = 0; 512 | 513 | const res = insertToTop( 514 | targetSourceNodeIndex, 515 | targetSourceNodes, 516 | originSourceNode, 517 | originSourceNodeIndex, 518 | targetSourceNodes, 519 | ); 520 | 521 | expect(res.targetSourceNodes).toEqual(['b', 'a', 'c', 'd']); 522 | }); 523 | 524 | it('case 4, different source and left most', () => { 525 | const targetSourceNodeIndex = 0; 526 | const targetSourceNodes = ['a', 'b', 'd', 'e']; 527 | const originSourceNode = 'c'; 528 | const originSourceNodeIndex = 1; 529 | const originSourceNodes = ['f', 'c']; 530 | 531 | const res = insertToTop( 532 | targetSourceNodeIndex, 533 | targetSourceNodes, 534 | originSourceNode, 535 | originSourceNodeIndex, 536 | originSourceNodes, 537 | ); 538 | 539 | expect(res.targetSourceNodes).toEqual(['c', 'a', 'b', 'd', 'e']); 540 | expect(res.originSourceNodes).toEqual(['f']); 541 | }); 542 | 543 | it('case 5, different source and left most', () => { 544 | const targetSourceNodeIndex = 0; 545 | const targetSourceNodes = ['a', 'b', 'd', 'e']; 546 | const originSourceNode = 'c'; 547 | const originSourceNodeIndex = 1; 548 | const originSourceNodes = ['f', 'c']; 549 | 550 | const res = insertToTop( 551 | targetSourceNodeIndex, 552 | targetSourceNodes, 553 | originSourceNode, 554 | originSourceNodeIndex, 555 | originSourceNodes, 556 | ); 557 | 558 | expect(res.targetSourceNodes).toEqual(['c', 'a', 'b', 'd', 'e']); 559 | expect(res.originSourceNodes).toEqual(['f']); 560 | }); 561 | it('case 6, different source and middle', () => { 562 | const targetSourceNodeIndex = 1; 563 | const targetSourceNodes = ['a', 'b', 'd', 'e']; 564 | const originSourceNode = 'c'; 565 | const originSourceNodeIndex = 1; 566 | const originSourceNodes = ['f', 'c']; 567 | 568 | const res = insertToTop( 569 | targetSourceNodeIndex, 570 | targetSourceNodes, 571 | originSourceNode, 572 | originSourceNodeIndex, 573 | originSourceNodes, 574 | ); 575 | 576 | expect(res.targetSourceNodes).toEqual(['a', 'c', 'b', 'd', 'e']); 577 | expect(res.originSourceNodes).toEqual(['f']); 578 | }); 579 | }); 580 | 581 | describe('sample case of move to bottom', () => { 582 | it('case 1, same source and leftmost', () => { 583 | const targetSourceNodeIndex = 0; 584 | const targetSourceNodes = ['a', 'b', 'c', 'd']; 585 | const originSourceNode = 'c'; 586 | const originSourceNodeIndex = 2; 587 | 588 | const res = insertToBottom( 589 | targetSourceNodeIndex, 590 | targetSourceNodes, 591 | originSourceNode, 592 | originSourceNodeIndex, 593 | targetSourceNodes, 594 | ); 595 | 596 | expect(res.targetSourceNodes).toEqual(['a', 'c', 'b', 'd']); 597 | }); 598 | 599 | it('case 2, same source and rightmost', () => { 600 | const targetSourceNodeIndex = 3; 601 | const targetSourceNodes = ['a', 'b', 'c', 'd']; 602 | const originSourceNode = 'a'; 603 | const originSourceNodeIndex = 0; 604 | 605 | const res = insertToBottom( 606 | targetSourceNodeIndex, 607 | targetSourceNodes, 608 | originSourceNode, 609 | originSourceNodeIndex, 610 | targetSourceNodes, 611 | ); 612 | 613 | expect(res.targetSourceNodes).toEqual(['b', 'c', 'd', 'a']); 614 | }); 615 | 616 | it('case 3, same source and middle', () => { 617 | const targetSourceNodeIndex = 1; 618 | const targetSourceNodes = ['a', 'b', 'c', 'd']; 619 | const originSourceNode = 'a'; 620 | const originSourceNodeIndex = 0; 621 | 622 | const res = insertToBottom( 623 | targetSourceNodeIndex, 624 | targetSourceNodes, 625 | originSourceNode, 626 | originSourceNodeIndex, 627 | targetSourceNodes, 628 | ); 629 | 630 | expect(res.targetSourceNodes).toEqual(['b', 'a', 'c', 'd']); 631 | }); 632 | 633 | it('case 4, different source and leftmost', () => { 634 | const targetSourceNodeIndex = 0; 635 | const targetSourceNodes = ['b', 'c', 'd']; 636 | const originSourceNode = 'a'; 637 | const originSourceNodeIndex = 1; 638 | const originSourceNodes = ['e', 'a']; 639 | 640 | const res = insertToBottom( 641 | targetSourceNodeIndex, 642 | targetSourceNodes, 643 | originSourceNode, 644 | originSourceNodeIndex, 645 | originSourceNodes, 646 | ); 647 | 648 | expect(res.targetSourceNodes).toEqual(['b', 'a', 'c', 'd']); 649 | expect(res.originSourceNodes).toEqual(['e']); 650 | }); 651 | 652 | it('case 5, different source and rightmost', () => { 653 | const targetSourceNodeIndex = 2; 654 | const targetSourceNodes = ['b', 'c', 'd']; 655 | const originSourceNode = 'a'; 656 | const originSourceNodeIndex = 1; 657 | const originSourceNodes = ['e', 'a']; 658 | 659 | const res = insertToBottom( 660 | targetSourceNodeIndex, 661 | targetSourceNodes, 662 | originSourceNode, 663 | originSourceNodeIndex, 664 | originSourceNodes, 665 | ); 666 | 667 | expect(res.targetSourceNodes).toEqual(['b', 'c', 'd', 'a']); 668 | expect(res.originSourceNodes).toEqual(['e']); 669 | }); 670 | 671 | it('case 6, different source and middle', () => { 672 | const targetSourceNodeIndex = 1; 673 | const targetSourceNodes = ['b', 'c', 'd']; 674 | const originSourceNode = 'a'; 675 | const originSourceNodeIndex = 1; 676 | const originSourceNodes = ['e', 'a']; 677 | 678 | const res = insertToBottom( 679 | targetSourceNodeIndex, 680 | targetSourceNodes, 681 | originSourceNode, 682 | originSourceNodeIndex, 683 | originSourceNodes, 684 | ); 685 | 686 | expect(res.targetSourceNodes).toEqual(['b', 'c', 'a', 'd']); 687 | expect(res.originSourceNodes).toEqual(['e']); 688 | }); 689 | }); 690 | }); 691 | -------------------------------------------------------------------------------- /src/tree/Tree.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import TreeNode from './TreeNode'; 4 | import iViewTemplate from './iviewTemplate'; 5 | 6 | import { TARGET_POSITION_TYPE } from './constants'; 7 | import { 8 | noop, 9 | formatSourceNodes, 10 | getDraggingNodesKey, 11 | calcDropPosition, 12 | computeMoveNeededParams, 13 | insertToTop, 14 | insertToBottom, 15 | } from './utils'; 16 | 17 | import './style.css'; 18 | 19 | export default Vue.component('Tree', { 20 | props: { 21 | value: { 22 | type: Array, 23 | }, 24 | autoExpandParent: { 25 | type: Boolean, 26 | default: true, 27 | }, 28 | // IE > 8 29 | draggable: { 30 | type: Boolean, 31 | default: false, 32 | }, 33 | onDragEnd: { 34 | type: Function, 35 | default: noop, 36 | }, 37 | onDragEnter: { 38 | type: Function, 39 | default: noop, 40 | }, 41 | onDragLeave: { 42 | type: Function, 43 | default: noop, 44 | }, 45 | onDragOver: { 46 | type: Function, 47 | default: noop, 48 | }, 49 | onDragStart: { 50 | type: Function, 51 | default: noop, 52 | }, 53 | onDrop: { 54 | type: Function, 55 | }, 56 | onExpand: { 57 | type: Function, 58 | default: noop, 59 | }, 60 | // custom children content 61 | template: { 62 | type: Function, 63 | default: iViewTemplate, 64 | }, 65 | beforeInner: { 66 | type: Function, 67 | }, 68 | beforeInsert: { 69 | type: Function, 70 | }, 71 | afterInsert: { 72 | type: Function, 73 | }, 74 | }, 75 | data() { 76 | this.draggingNodesKeys = []; 77 | 78 | return { 79 | dragOverNodeKey: '', 80 | dropPosition: '', 81 | expandedKeys: [], 82 | }; 83 | }, 84 | computed: { 85 | data() { 86 | return this.value; 87 | }, 88 | }, 89 | methods: { 90 | /** 91 | * every tree node is rendered by this method 92 | * @param {FormattedSourceNode} formattedSourceNode 93 | * @param {number} index - map index 94 | * @return {VNode} 95 | */ 96 | renderTreeNode(formattedSourceNode) { 97 | const { 98 | key, 99 | title, 100 | pos, 101 | children, 102 | } = formattedSourceNode; 103 | 104 | // the flag show node status(at node top or bottom) when dragging 105 | const dragOverGapTop = ( 106 | this.dragOverNodeKey === key 107 | && this.dropPosition === TARGET_POSITION_TYPE.TOP 108 | ); 109 | const dragOverGapBottom = ( 110 | this.dragOverNodeKey === key 111 | && this.dropPosition === TARGET_POSITION_TYPE.BOTTOM 112 | ); 113 | const dragOver = ( 114 | this.dragOverNodeKey === key 115 | && this.dropPosition === TARGET_POSITION_TYPE.CONTENT 116 | ); 117 | 118 | // is expend 119 | const expanded = this.expandedKeys.indexOf(key) !== -1; 120 | 121 | return this.$createElement(TreeNode, { 122 | props: { 123 | rckey: key, 124 | title, 125 | // pass tree root instance to child 126 | root: this, 127 | pos, 128 | children, 129 | dragOver, 130 | dragOverGapTop, 131 | dragOverGapBottom, 132 | template: this.template, 133 | draggable: this.draggable, 134 | expanded, 135 | source: formattedSourceNode, 136 | }, 137 | }); 138 | }, 139 | /** 140 | * @param {Event} e 141 | * @param {VueComponent} treeNode - dragging node 142 | */ 143 | handleStartDrag(e, treeNode) { 144 | this.draggingNode = treeNode; 145 | this.draggingNodesKeys = getDraggingNodesKey(treeNode); 146 | this.onDragStart({ 147 | event: e, 148 | node: treeNode, 149 | }); 150 | }, 151 | handleNodeEntered(e, treeNode) { 152 | // get the position to be place 153 | const dropPosition = calcDropPosition(e, treeNode); 154 | // if dragging node is the entered node 155 | if ( 156 | this.draggingNode.rckey === treeNode.rckey 157 | && dropPosition === 0 158 | ) { 159 | this.dragOverNodeKey = ''; 160 | this.dropPosition = null; 161 | return; 162 | } 163 | 164 | this.dragOverNodeKey = treeNode.rckey; 165 | this.dropPosition = dropPosition; 166 | this.onDragEnter({ 167 | event: e, 168 | node: treeNode, 169 | }); 170 | }, 171 | handleNodeCrossed(e, treeNode) { 172 | this.onDragOver({ event: e, node: treeNode }); 173 | }, 174 | handleNodeLeaved(e, treeNode) { 175 | this.onDragLeave({ event: e, node: treeNode }); 176 | }, 177 | /** 178 | * drop tree node 179 | * @param {Event} e 180 | * @param {VueComponent} treeNode - dropped node 181 | */ 182 | handleNodeDropped(e, treeNode) { 183 | const { rckey } = treeNode; 184 | const targetPosition = this.dropPosition; 185 | 186 | this.dragOverNodeKey = ''; 187 | // if drop node to its children node 188 | if (this.draggingNodesKeys.includes(rckey)) { 189 | console.error('Can not drop to dragNode(include it\'s children node)'); 190 | return; 191 | } 192 | 193 | const res = { 194 | event: e, 195 | node: treeNode, 196 | dragNode: this.draggingNode, 197 | targetPosition, 198 | }; 199 | const isDropToGap = targetPosition !== TARGET_POSITION_TYPE.CONTENT; 200 | if (isDropToGap) { 201 | res.dropToGap = true; 202 | } 203 | if (this.onDrop) { 204 | this.onDrop(res); 205 | return; 206 | } 207 | 208 | const targetNodeKey = rckey; 209 | const draggingNodeKey = this.draggingNode.rckey; 210 | const sourceNodes = [...this.data]; 211 | 212 | const { 213 | targetSourceNode, 214 | targetSourceNodeIndex, 215 | targetSourceNodes, 216 | originSourceNode, 217 | originSourceNodeIndex, 218 | originSourceNodes, 219 | } = computeMoveNeededParams( 220 | sourceNodes, 221 | draggingNodeKey, 222 | targetNodeKey, 223 | targetPosition, 224 | ); 225 | // insert to content 226 | if (targetPosition === TARGET_POSITION_TYPE.CONTENT) { 227 | if (this.beforeInner) { 228 | this.beforeInner( 229 | 'inner', 230 | targetSourceNode.children, 231 | originSourceNode, 232 | ); 233 | return; 234 | } 235 | targetSourceNode.children = targetSourceNode.children || []; 236 | targetSourceNode.children.push(originSourceNode); 237 | originSourceNodes.splice( 238 | originSourceNodeIndex, 239 | 1, 240 | ); 241 | } 242 | if (this.beforeInsert) { 243 | this.beforeInsert( 244 | 'insert', 245 | targetSourceNodes, 246 | targetSourceNodeIndex, 247 | originSourceNode, 248 | ); 249 | return; 250 | } 251 | // move to top 252 | if (targetPosition === TARGET_POSITION_TYPE.TOP) { 253 | insertToTop( 254 | targetSourceNodeIndex, 255 | targetSourceNodes, 256 | originSourceNode, 257 | originSourceNodeIndex, 258 | originSourceNodes, 259 | ); 260 | } 261 | // move to bottom 262 | if (targetPosition === TARGET_POSITION_TYPE.BOTTOM) { 263 | insertToBottom( 264 | targetSourceNodeIndex, 265 | targetSourceNodes, 266 | originSourceNode, 267 | originSourceNodeIndex, 268 | originSourceNodes, 269 | ); 270 | } 271 | this.$emit('input', sourceNodes); 272 | if (this.afterInsert) { 273 | this.afterInsert(); 274 | } 275 | }, 276 | handleDragEnd(e, treeNode) { 277 | this.dragOverNodeKey = ''; 278 | this.$emit('dragEnd', this.data, treeNode, e); 279 | }, 280 | 281 | /** 282 | * 283 | * @param {VueComponent} treeNode 切换展开状态的节点 284 | */ 285 | expand(treeNode) { 286 | const expanded = !treeNode.expanded; 287 | const expandedKeys = [...this.expandedKeys]; 288 | const rckey = treeNode.rckey; 289 | const index = expandedKeys.indexOf(rckey); 290 | // 如果点击的节点要展开,但是不在表示已经展开的 expandKeys 数组中 291 | if (expanded && index === -1) { 292 | // 就加入该数组,在重新渲染的时候,就会展开了 293 | expandedKeys.push(rckey); 294 | } else if (!expanded && index > -1) { 295 | expandedKeys.splice(index, 1); 296 | } 297 | this.expandedKeys = expandedKeys; 298 | this.onExpand(expandedKeys, { node: treeNode, expanded }); 299 | }, 300 | }, 301 | render() { 302 | const formattedSourceNodes = formatSourceNodes(this.data); 303 | /** 304 | * 1、first render root node 305 | * 2、if node has children, render by itself 306 | */ 307 | return ( 308 |
    313 | {formattedSourceNodes.map(this.renderTreeNode)} 314 |
315 | ); 316 | }, 317 | }); 318 | -------------------------------------------------------------------------------- /src/tree/TreeNode.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import classNames from 'classnames'; 3 | 4 | import arrow from './arrow.svg'; 5 | 6 | const defaultTitle = '---'; 7 | const defaultPrefixCls = 'rc'; 8 | 9 | const TreeNode = Vue.component('TreeNode', { 10 | props: { 11 | prefixCls: { 12 | type: String, 13 | default() { 14 | return defaultPrefixCls; 15 | }, 16 | }, 17 | source: { 18 | type: Object, 19 | }, 20 | title: { 21 | type: String, 22 | default() { 23 | return defaultTitle; 24 | }, 25 | }, 26 | rckey: { 27 | }, 28 | pos: { 29 | type: String, 30 | }, 31 | // 父节点 32 | root: { 33 | type: Object, 34 | }, 35 | children: { 36 | type: Array, 37 | }, 38 | draggable: { 39 | type: Boolean, 40 | default: false, 41 | }, 42 | dragOver: {}, 43 | dragOverGapTop: { 44 | type: Boolean, 45 | }, 46 | dragOverGapBottom: { 47 | type: Boolean, 48 | }, 49 | template: {}, 50 | // 展开收起状态 51 | expanded: { 52 | type: Boolean, 53 | }, 54 | }, 55 | data() { 56 | this.isTreeNode = true; 57 | return { 58 | dataLoading: false, 59 | dragNodeHighlight: false, 60 | }; 61 | }, 62 | computed: { 63 | // 将正在拖拽的节点暴露在 this 上 64 | handleSelect() { 65 | return this.$refs.handleSelect; 66 | }, 67 | }, 68 | methods: { 69 | /** 70 | * 渲染子节点 71 | */ 72 | renderChildren() { 73 | const { expanded, children } = this; 74 | let newchildren = null; 75 | if (children && !expanded) { 76 | newchildren =
    77 | {children.map(formattedSourceNode => 78 | this.root.renderTreeNode(formattedSourceNode), 79 | )} 80 |
; 81 | } 82 | return newchildren; 83 | }, 84 | onSelect() { 85 | this.root.onSelect(this); 86 | }, 87 | onDragStart(e) { 88 | // console.log(this.title, 'drag start'); 89 | e.stopPropagation(); 90 | this.dragNodeHighlight = true; 91 | this.root.handleStartDrag(e, this); 92 | try { 93 | // ie throw error 94 | // firefox-need-it 95 | e.dataTransfer.setData('text/plain', ''); 96 | } catch (error) { 97 | // empty 98 | } 99 | }, 100 | onDragEnter(e) { 101 | // console.log(this.title, 'drag enter', e.target); 102 | e.preventDefault(); 103 | e.stopPropagation(); 104 | this.root.handleNodeEntered(e, this); 105 | }, 106 | onDragOver(e) { 107 | // console.log(this.title, 'drag over', e.target); 108 | e.preventDefault(); 109 | e.stopPropagation(); 110 | this.root.handleNodeCrossed(e, this); 111 | }, 112 | onDragLeave(e) { 113 | // console.log(this.title, 'drag leave', e.target); 114 | e.stopPropagation(); 115 | this.root.handleNodeLeaved(e, this); 116 | }, 117 | onDrop(e) { 118 | // console.log(this.title, 'drop', e.target); 119 | e.preventDefault(); 120 | e.stopPropagation(); 121 | this.dragNodeHighlight = false; 122 | this.root.handleNodeDropped(e, this); 123 | }, 124 | onDragEnd(e) { 125 | // console.log(this.title, 'drag end', e.target); 126 | e.stopPropagation(); 127 | this.dragNodeHighlight = false; 128 | this.root.handleDragEnd(e, this); 129 | }, 130 | onExpand() { 131 | const callbackPromise = this.root.expand(this); 132 | if (callbackPromise && typeof callbackPromise === 'object') { 133 | const setLoading = (dataLoading) => { 134 | this.dataLoading = dataLoading; 135 | }; 136 | setLoading(true); 137 | callbackPromise.then(() => { 138 | setLoading(false); 139 | }, () => { 140 | setLoading(false); 141 | }); 142 | } 143 | }, 144 | switcher() { 145 | let state = ''; 146 | if (this.children && this.children.length > 0) { 147 | if (this.expanded) { 148 | state += ' ivu-tree-arrow-open'; 149 | } 150 | } 151 | return ( 152 | 156 | { 157 | (this.children && this.children.length) 158 | ? 159 | : null 160 | } 161 | 162 | ); 163 | }, 164 | }, 165 | render(h) { 166 | // render draggable part 167 | const selectHandle = () => { 168 | const content = this.title; 169 | const Component = this.template; 170 | return h('span', { 171 | ref: 'selectHandle', 172 | attrs: { 173 | class: 'ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal draggable', 174 | draggable: this.draggable, 175 | }, 176 | on: { 177 | dragstart: this.onDragStart, 178 | dragenter: this.onDragEnter, 179 | dragover: this.onDragOver, 180 | dragleave: this.onDragLeave, 181 | drop: this.onDrop, 182 | dragend: this.onDragEnd, 183 | }, 184 | }, [ 185 | h(Component, { 186 | attrs: {}, 187 | props: { 188 | title: content, 189 | node: this.source, 190 | }, 191 | }, []), 192 | ]); 193 | }; 194 | let dragOverCls = ''; 195 | if (this.disabled) { 196 | // disabledCls = `${prefixCls}-treenode-disabled`; 197 | } else if (this.dragOver) { 198 | dragOverCls = 'drag-over'; 199 | } else if (this.dragOverGapTop) { 200 | dragOverCls = 'drag-over-gap-top'; 201 | } else if (this.dragOverGapBottom) { 202 | dragOverCls = 'drag-over-gap-bottom'; 203 | } 204 | 205 | return h('li', { 206 | class: classNames(dragOverCls), 207 | }, [this.switcher(), selectHandle(), this.renderChildren()]); 208 | }, 209 | }); 210 | 211 | export default TreeNode; 212 | -------------------------------------------------------------------------------- /src/tree/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tree/constants.js: -------------------------------------------------------------------------------- 1 | export const TARGET_POSITION_TYPE = { 2 | TOP: 1, 3 | BOTTOM: -1, 4 | CONTENT: 0, 5 | }; 6 | 7 | export default { 8 | TARGET_POSITION_TYPE, 9 | }; 10 | -------------------------------------------------------------------------------- /src/tree/defaultTemplate.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default Vue.component('DEFAULT_TEMPLATE', { 4 | props: { 5 | title: { 6 | }, 7 | node: { 8 | type: Object, 9 | }, 10 | }, 11 | render() { 12 | return (
{this.title}
); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/tree/index.js: -------------------------------------------------------------------------------- 1 | import Tree from './Tree'; 2 | 3 | export { 4 | computeMoveNeededParams, 5 | findSourceNodeByKey, 6 | insertToBottom, 7 | insertToTop, 8 | } from './utils'; 9 | export { 10 | TARGET_POSITION_TYPE, 11 | } from './constants'; 12 | 13 | export default Tree; 14 | -------------------------------------------------------------------------------- /src/tree/iviewTemplate.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default Vue.component('IVIEW_TEMPLATE', { 4 | props: { 5 | title: { 6 | }, 7 | node: { 8 | type: Object, 9 | }, 10 | }, 11 | render() { 12 | return ( 13 | {this.title} 14 | ); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/tree/style.css: -------------------------------------------------------------------------------- 1 | ul, li { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | li { 6 | list-style: none; 7 | } 8 | .draggable { 9 | line-height: 14px; 10 | } 11 | 12 | .drag-over>.draggable { 13 | background: #2d8cf0; 14 | } 15 | .drag-over-gap-top>.draggable { 16 | border-top: 2px solid #2b85e4; 17 | } 18 | .drag-over-gap-bottom>.draggable { 19 | border-bottom: 2px solid #2b85e4; 20 | } 21 | 22 | .ivu-tree ul li { 23 | list-style: none; 24 | margin: 8px 0; 25 | padding: 0; 26 | white-space: nowrap; 27 | outline: 0; 28 | } 29 | .ivu-tree-arrow { 30 | cursor: pointer; 31 | width: 12px; 32 | text-align: center; 33 | display: inline-block; 34 | } 35 | .ivu-tree-arrow-open img { 36 | transform: rotate(-90deg); 37 | } 38 | .ivu-tree-arrow i { 39 | transition: all .2s ease-in-out; 40 | } 41 | 42 | .ivu-icon-arrow-right-b { 43 | background: url(../assets/arrow.svg); 44 | } 45 | /* title */ 46 | .ivu-tree-title { 47 | display: inline-block; 48 | margin: 0; 49 | padding: 0 4px; 50 | border-radius: 3px; 51 | cursor: pointer; 52 | vertical-align: top; 53 | color: #495060; 54 | transition: all .2s ease-in-out; 55 | } 56 | .ivu-tree-title:hover { 57 | background-color: #eaf4fe; 58 | } 59 | .ivu-tree li ul { 60 | margin: 0; 61 | padding: 0 0 0 18px; 62 | } 63 | .ivu-tree li { 64 | white-space: nowrap; 65 | } 66 | .ivu-tree ul { 67 | list-style: none; 68 | margin: 0; 69 | padding: 0; 70 | /* font-size: 12px; */ 71 | } 72 | .ivu-article li:not([class^=ivu-]) { 73 | margin-bottom: 5px; 74 | font-size: 14px; 75 | } 76 | 77 | /** ant design **/ 78 | .ant-tree { 79 | margin: 0; 80 | padding: 0; 81 | font-size: 12px; 82 | } 83 | .ant-tree li { 84 | padding: 4px 0; 85 | margin: 0; 86 | list-style: none; 87 | white-space: nowrap; 88 | outline: 0; 89 | } 90 | .ant-tree li ul { 91 | margin: 0; 92 | padding: 0 0 0 18px; 93 | } 94 | 95 | .ant-tree li span[draggable=true], .ant-tree li span[draggable] { 96 | -webkit-user-select: none; 97 | -moz-user-select: none; 98 | -ms-user-select: none; 99 | user-select: none; 100 | border-top: 2px solid transparent; 101 | border-bottom: 2px solid transparent; 102 | margin-top: -2px; 103 | -khtml-user-drag: element; 104 | -webkit-user-drag: element; 105 | } 106 | .ant-tree li .ant-tree-node-content-wrapper { 107 | display: inline-block; 108 | padding: 3px 5px; 109 | border-radius: 2px; 110 | margin: 0; 111 | cursor: pointer; 112 | text-decoration: none; 113 | vertical-align: top; 114 | color: rgba(0,0,0,.65); 115 | -webkit-transition: all .3s; 116 | transition: all .3s; 117 | position: relative; 118 | } 119 | .ant-tree-node-content-wrapper { 120 | width: calc(100% - 18px); 121 | } 122 | .ant-tree li .ant-tree-node-content-wrapper:hover { 123 | background-color: #ecf6fd; 124 | } 125 | .ant-tree li.drag-over-gap-top>span[draggable] { 126 | border-top-color: #108ee9; 127 | } 128 | .ant-tree li.drag-over-gap-bottom>span[draggable] { 129 | border-bottom-color: #108ee9; 130 | } 131 | 132 | -------------------------------------------------------------------------------- /src/tree/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | TARGET_POSITION_TYPE, 3 | } from './constants'; 4 | 5 | export function noop() {} 6 | /** 7 | * type NodeLevel = string; // like 0、0-0、0-1、0-0-0 8 | * interface SourceNode { 9 | * key: string; 10 | * title: string; 11 | * children?: Array; 12 | * } 13 | * interface FormattedSourceNode { 14 | * key: string; 15 | * title: string; 16 | * pos: string; 17 | * children?: Array; 18 | * [propsName: string]: any; 19 | * } 20 | */ 21 | /** 22 | * add some key to sourceNode 23 | * @param {Array} data 24 | * @param {string} [level='0'] - level at tree 25 | * @return {Array} 26 | */ 27 | export const formatSourceNodes = ( 28 | sourceNodes, 29 | level = 1, 30 | parentPos, 31 | ) => sourceNodes.map((sourceNode, i) => { 32 | const { key, title, ...restProps } = sourceNode; 33 | const formattedSourceNode = { 34 | ...restProps, 35 | key, 36 | title, 37 | pos: parentPos === undefined ? String(i) : `${parentPos}-${i}`, 38 | }; 39 | if (sourceNode.children && sourceNode.children.length) { 40 | const nextLevel = level + 1; 41 | formattedSourceNode.children = formatSourceNodes( 42 | sourceNode.children, 43 | nextLevel, 44 | formattedSourceNode.pos, 45 | ); 46 | } 47 | return formattedSourceNode; 48 | }); 49 | 50 | /** 51 | * collect node key and its children keys 52 | * @param {Array} treeNodes 53 | * @param {function} callback 54 | */ 55 | export function traverseTreeNodes(treeNodes = [], callback) { 56 | /** 57 | * @param {Array} subTreeNodes 58 | * @param {number} level 59 | * @param {Array<>} parentsChildrenPos 60 | * @param {number} parentPos 61 | */ 62 | function traverse(subTreeNodes, level, parentsChildrenPos, parentPos) { 63 | let newSubTreeNodes = subTreeNodes; 64 | if (subTreeNodes && subTreeNodes.length) { 65 | newSubTreeNodes = subTreeNodes.filter(Boolean); 66 | } 67 | 68 | newSubTreeNodes.forEach((treeNode, index) => { 69 | if (!treeNode.isTreeNode) { 70 | return; 71 | } 72 | const { pos } = treeNode; 73 | // Note: side effect 74 | parentsChildrenPos.push(pos); 75 | 76 | const childrenPos = []; 77 | if (treeNode.$children) { 78 | traverse(treeNode.$children, pos, childrenPos, pos); 79 | } 80 | callback( 81 | treeNode, 82 | index, 83 | pos, 84 | treeNode.rckey || pos, 85 | childrenPos, 86 | parentPos, 87 | ); 88 | }); 89 | } 90 | traverse(treeNodes, 0, []); 91 | } 92 | 93 | /** 94 | * 95 | * @param {*} smallArray 96 | * @param {*} bigArray 97 | */ 98 | export function isInclude(smallArray, bigArray) { 99 | return smallArray.every((item, index) => item === bigArray[index]); 100 | } 101 | 102 | /** 103 | * get key and children's key of dragging node 104 | * @param {VueComponent} treeNode - dragging node 105 | * @return {Array<>} 106 | */ 107 | export function getDraggingNodesKey(treeNode) { 108 | const dragNodesKeys = []; 109 | // 拿到位置信息 110 | const treeNodePosArr = treeNode.pos.split('-'); 111 | traverseTreeNodes(treeNode.$children, (item, index, pos, key) => { 112 | const childPosArr = pos.split('-'); 113 | if ( 114 | ( 115 | treeNode.pos === pos || 116 | treeNodePosArr.length < childPosArr.length 117 | ) 118 | && isInclude(treeNodePosArr, childPosArr) 119 | ) { 120 | // 正在拖拽的节点的“子孙节点” 121 | dragNodesKeys.push(key); 122 | } 123 | }); 124 | // 再将正在拖拽的节点 key 放进来 125 | dragNodesKeys.push(treeNode.rckey); 126 | return dragNodesKeys; 127 | } 128 | 129 | /** 130 | * get node position info 131 | * @param {Element} ele 132 | */ 133 | export function getOffset(ele) { 134 | if (!ele.getClientRects().length) { 135 | return { top: 0, left: 0 }; 136 | } 137 | 138 | const rect = ele.getBoundingClientRect(); 139 | if (rect.width || rect.height) { 140 | const doc = ele.ownerDocument; 141 | const win = doc.defaultView; 142 | const docElem = doc.documentElement; 143 | 144 | return { 145 | top: (rect.top + win.pageYOffset) - docElem.clientTop, 146 | left: (rect.left + win.pageXOffset) - docElem.clientLeft, 147 | }; 148 | } 149 | 150 | return rect; 151 | } 152 | 153 | /** 154 | * type TargetPositionType = -1 | 0 | 1; 155 | */ 156 | 157 | /** 158 | * @param {Event} e 159 | * @param {VueComponent} treeNode - entered node 160 | * @return {TargetPostionType} 161 | * TARGET_POSITION_TYPE.BOTTOM 162 | * |TARGET_POSITION_TYPE.CONTENT 163 | * |TARGET_POSITION_TYPE.TOP 164 | */ 165 | export function calcDropPosition(e, treeNode) { 166 | const { selectHandle } = treeNode.$refs; 167 | const offsetTop = getOffset(selectHandle).top; 168 | const offsetHeight = selectHandle.offsetHeight; 169 | const pageY = e.pageY; 170 | // TODO: remove hard code 171 | const gapHeight = 2; 172 | // if move to node bottom 173 | if (pageY > ((offsetTop + offsetHeight) - gapHeight)) { 174 | return TARGET_POSITION_TYPE.BOTTOM; 175 | } 176 | // if move to node top 177 | if (pageY < offsetTop + gapHeight) { 178 | return TARGET_POSITION_TYPE.TOP; 179 | } 180 | // move to node content 181 | return TARGET_POSITION_TYPE.CONTENT; 182 | } 183 | 184 | /** 185 | * interface FindSourceCallback { 186 | * (sourceNode: SourceNode, index: number, arr: Array): void; 187 | * } 188 | */ 189 | /** 190 | * @param {Array} data 191 | * @param {string} key 192 | * @param {FindSourceCallback} callback 193 | */ 194 | export const findSourceNodeByKey = (sourceNodes, key, callback) => { 195 | sourceNodes.forEach((sourceNode, index, arr) => { 196 | if (sourceNode.key === key) { 197 | return callback(sourceNode, index, arr); 198 | } 199 | if (sourceNode.children) { 200 | return findSourceNodeByKey(sourceNode.children, key, callback); 201 | } 202 | return false; 203 | }); 204 | }; 205 | 206 | /** 207 | * get last sourceNodes and move type 208 | * @param {Array} sourceNodes 209 | * @param {any} draggingNodeKey 210 | * @param {any} targetNodeKey 211 | * @param {TargetPostionType} targetPosition 212 | * @return {SourceNode | undefined} targetNode 213 | * @return {number | undefined} targetNodeIndex 214 | * @return {Array | undefined} targetNodes 215 | * @return {SourceNode} originSourceNode 216 | * @return {number} originSourceNodeIndex 217 | * @return {Array} originSourceNodes 218 | */ 219 | export function computeMoveNeededParams( 220 | sourceNodes, 221 | draggingNodeKey, 222 | targetNodeKey, 223 | targetPosition, 224 | ) { 225 | const isDropToGap = targetPosition !== TARGET_POSITION_TYPE.CONTENT; 226 | let draggingSourceNode; 227 | let hasSameLevelNodesAsDraggingNode; 228 | let draggingNodeIndexAtSameLevelNodes; 229 | // first we find the dragging sourceNode 230 | findSourceNodeByKey(sourceNodes, draggingNodeKey, (sourceNode, index, arr) => { 231 | hasSameLevelNodesAsDraggingNode = arr; 232 | draggingNodeIndexAtSameLevelNodes = index; 233 | draggingSourceNode = sourceNode; 234 | }); 235 | let hasSameLevelNodesAsTargetNode = null; 236 | let targetNodeIndexAtSameLevelNodes; 237 | if (!isDropToGap) { 238 | let targetSourceNode = null; 239 | // place to target content, mean become child of target node 240 | const findSourceNodeCallback = (sourceNode) => { 241 | targetSourceNode = sourceNode; 242 | }; 243 | findSourceNodeByKey(sourceNodes, targetNodeKey, findSourceNodeCallback); 244 | return { 245 | targetSourceNode, 246 | originSourceNode: draggingSourceNode, 247 | originSourceNodeIndex: draggingNodeIndexAtSameLevelNodes, 248 | originSourceNodes: hasSameLevelNodesAsDraggingNode, 249 | }; 250 | } 251 | // remove source node from same level nodes 252 | const findSourceNodeCallback = (_, index, nodes) => { 253 | hasSameLevelNodesAsTargetNode = nodes; 254 | targetNodeIndexAtSameLevelNodes = index; 255 | }; 256 | findSourceNodeByKey(sourceNodes, targetNodeKey, findSourceNodeCallback); 257 | 258 | return { 259 | targetSourceNodes: hasSameLevelNodesAsTargetNode, 260 | targetSourceNodeIndex: targetNodeIndexAtSameLevelNodes, 261 | originSourceNode: draggingSourceNode, 262 | originSourceNodeIndex: draggingNodeIndexAtSameLevelNodes, 263 | originSourceNodes: hasSameLevelNodesAsDraggingNode, 264 | }; 265 | } 266 | 267 | /** 268 | * param reassign, no return 269 | * @param {number} targetSourceNodeIndex 270 | * @param {Array} targetSourceNodes 271 | * @param {SourceNode} originSourceNode 272 | * @param {number} originSourceNodeIndex 273 | * @param {Array} originSourceNodes 274 | */ 275 | export function insertToTop( 276 | targetSourceNodeIndex, 277 | targetSourceNodes, 278 | originSourceNode, 279 | originSourceNodeIndex, 280 | originSourceNodes, 281 | ) { 282 | if ( 283 | originSourceNodes !== targetSourceNodes 284 | || originSourceNodeIndex > targetSourceNodeIndex 285 | ) { 286 | originSourceNodes.splice(originSourceNodeIndex, 1); 287 | targetSourceNodes.splice( 288 | targetSourceNodeIndex, 289 | 0, 290 | originSourceNode, 291 | ); 292 | return { 293 | targetSourceNodes, 294 | originSourceNodes, 295 | }; 296 | } 297 | targetSourceNodes.splice( 298 | targetSourceNodeIndex, 299 | 0, 300 | originSourceNode, 301 | ); 302 | originSourceNodes.splice(originSourceNodeIndex, 1); 303 | return { 304 | targetSourceNodes, 305 | originSourceNodes, 306 | }; 307 | } 308 | 309 | /** 310 | * param reassign, no return 311 | * @param {number} targetSourceNodeIndex 312 | * @param {Array} targetSourceNodes 313 | * @param {SourceNode} originSourceNode 314 | * @param {number} originSourceNodeIndex 315 | * @param {Array} originSourceNodes 316 | */ 317 | export function insertToBottom( 318 | targetSourceNodeIndex, 319 | targetSourceNodes, 320 | originSourceNode, 321 | originSourceNodeIndex, 322 | originSourceNodes, 323 | ) { 324 | let newTargetSourceNodeIndex = targetSourceNodeIndex + 1; 325 | if (originSourceNodes === targetSourceNodes) { 326 | if (targetSourceNodeIndex > originSourceNodeIndex) { 327 | newTargetSourceNodeIndex = targetSourceNodeIndex; 328 | } 329 | } 330 | originSourceNodes.splice(originSourceNodeIndex, 1); 331 | targetSourceNodes.splice( 332 | newTargetSourceNodeIndex, 333 | 0, 334 | originSourceNode, 335 | ); 336 | return { 337 | targetSourceNodes, 338 | originSourceNodes, 339 | }; 340 | } 341 | -------------------------------------------------------------------------------- /static/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: './src/tree/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'vue-draggable-tree.min.js', 9 | library: 'VueDraggableTree', 10 | libraryTarget: 'umd', 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.vue$/, 16 | loader: 'vue', 17 | }, 18 | { 19 | test: /\.js$/, 20 | loader: 'babel-loader', 21 | include: [path.join(__dirname, 'src')], 22 | exclude: /node_modules/, 23 | }, 24 | { 25 | test: /\.css$/, 26 | loader: 'style-loader!css-loader', 27 | include: [path.join(__dirname, 'src')], 28 | exclude: /node_modules/, 29 | }, 30 | { 31 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 32 | loader: 'url-loader', 33 | }, 34 | ], 35 | }, 36 | externals: { 37 | vue: 'vue', 38 | classnames: 'classnames', 39 | }, 40 | plugins: [ 41 | new webpack.optimize.UglifyJsPlugin({ 42 | compress: { 43 | warnings: false, 44 | }, 45 | }), 46 | ], 47 | }; 48 | --------------------------------------------------------------------------------