├── .eslintrc.js ├── .github └── workflows │ ├── example.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README-ZH.md ├── README.md ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── ElTableDraggable.vue │ └── ListViewer.vue ├── examples │ ├── Base.vue │ ├── Column.vue │ ├── ColumnWithHandler.vue │ ├── DataTree.vue │ ├── DataTreeSampleParent.vue │ ├── Handle.vue │ ├── MultiTable.vue │ ├── MultiTableClone.vue │ ├── MultiTableGroup.vue │ ├── MultiTableNestint.vue │ └── SimpleTableNestint.vue ├── main.js └── utils │ ├── createTable.js │ ├── dom.js │ ├── manualRowExchange.js │ ├── options │ ├── column.js │ ├── constant.js │ ├── index.js │ └── row.js │ ├── ua.js │ └── utils.js ├── types └── DomInfo.d.ts ├── vetur ├── attributes.json └── tags.json └── vue.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:vue/essential" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 11, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "vue" 16 | ], 17 | "rules": { 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/example.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: EXAMPLE 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | build: 19 | # The type of runner that the job will run on 20 | runs-on: ubuntu-latest 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm run build:demo 32 | - name: Deploy to GitHub Pages 33 | uses: crazy-max/ghaction-github-pages@v2 34 | with: 35 | target_branch: gh-pages 36 | build_dir: examples 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm run build 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm run build 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: RELEASE 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | tags: "v*" 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-node@v1 26 | with: 27 | node-version: 12 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm ci 30 | - run: npm run build 31 | - name: GH Release 32 | uses: softprops/action-gh-release@v0.1.5 33 | with: 34 | # Creates a draft release. Defaults to false 35 | draft: true 36 | # Newline-delimited list of path globs for asset files to upload 37 | files: | 38 | dist/* 39 | package.json 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /examples 5 | 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 yiwei.wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | # el-table-draggable 2 | 3 | 让`vue-draggable`支持`element-ui`中的`el-table` 4 | 5 | [demo 请查看](https://www.mizuka.top/el-table-draggable/) 6 | 7 | 有问题请提交issue!我不是义务解答员 8 | 9 | 欢迎提mr改进 10 | 11 | ## 已知问题 12 | 13 | 建议使用树状表格拖拽的人不要使用本插件 14 | 15 | 1. 树状表格向下拖拽/跨越层级拖拽时候move/end的显示结果不一致 16 | 2. 树状表格无法判断是拖入子级/同级 17 | 18 | ## 特性 19 | 20 | - 支持几乎所有`sortablejs`的配置 21 | - 支持多个表格之间互相拖动 22 | - 代码提示 23 | - 针对空行进行了处理,可以直接拖动到空的 el-table 内,无论你有没有显示空行的提示行,默认高度为 60px,可以靠`.el-table-draggable__empty-table {min-height: px;}`来自定义 24 | 25 | ### 目前支持的特性 26 | 27 | - 行拖拽 28 | - 列拖拽(>1.1.0) 29 | - 设置 handle 30 | - 设置 group 实现分类拖拽 31 | - 树状表格拖拽 (>1.2.0) 32 | - onMove 支持 (>1.3.0) 33 | - ...其他 sortable.js 的配置 34 | - input 事件,因为 change 事件和 sortable.js 的默认事件重复,改为 input 事件,回调为变化后的行(列)集合 35 | 36 | ## 安装 37 | 38 | ### 使用 npm 或者 yarn 39 | 40 | ```bash 41 | yarn add el-table-draggable 42 | 43 | npm i -S el-table-draggable 44 | ``` 45 | 46 | ## 使用 47 | 48 | ```js 49 | import ElTableDraggable from "el-table-draggable"; 50 | 51 | export default { 52 | components: { 53 | ElTableDraggable, 54 | }, 55 | }; 56 | ``` 57 | 58 | ### template 59 | 60 | ```html 61 | 66 | ``` 67 | 68 | ### props 69 | 70 | #### tag 71 | 72 | 包裹的组件,默认为 div 73 | 74 | #### column 75 | 76 | 启用列拖拽(试验性功能)0.6 增加 77 | 78 | #### onMove 79 | 80 | 支持`onMove`回调 81 | 82 | ```javascript 83 | onMove: function (/**Event*/evt, /**Event*/originalEvent, domInfo) { 84 | // Example: https://jsbin.com/nawahef/edit?js,output 85 | evt.dragged; // dragged HTMLElement 86 | evt.draggedRect; // DOMRect {left, top, right, bottom} 87 | evt.related; // HTMLElement on which have guided 88 | evt.relatedRect; // DOMRect 89 | evt.willInsertAfter; // Boolean that is true if Sortable will insert drag element after target by default 90 | originalEvent.clientY; // mouse position 91 | 92 | domInfo.dragged // 拖拽的行的基本信息,包含其所属data,dataindex,parent是哪个domInfo 93 | domInfo.related // 根据算法算出来的对应dom,树状表格下可能和屏幕上显示的dom不一致 94 | 95 | // return false; — for cancel 96 | // return -1; — insert before target 97 | // return 1; — insert after target 98 | }, 99 | ``` 100 | 101 | #### 其他 102 | 103 | 差不多支持所有[sortablejs 的 option](https://github.com/SortableJS/Sortable#options) 104 | 105 | ### 事件 106 | 107 | #### input 108 | 109 | 内部 table 的数据有因为拖动造成的顺序,增减时进行通知 110 | 111 | 0.5 增加 112 | 113 | 回调为当前所有行数据 114 | 115 | 0.6 新增 116 | 117 | 列模式下,如果有`value`,返回`value`, 否则返回当前列属性列表(property) 118 | 119 | #### 其他 120 | 121 | 差不多支持所有[sortablejs 的 option](https://github.com/SortableJS/Sortable#options)里面那些`on`开头的事件,绑定事件的时候请去掉`on` 例如`onSort => @sort` 122 | 123 | ## todo 124 | 125 | - [ ] 改进树状表格拖拽 126 | 127 | ## 捐赠 128 | 129 | [请我喝咖啡](https://buymeacoffee.com/mizukawu) 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # el-table-draggable 2 | 3 | [中文文档](./README-ZH.md) 4 | 5 | Let el-table support sortable.js 6 | 7 | [Demo Page](https://www.mizuka.top/el-table-draggable/) 8 | 9 | ## Features 10 | 11 | - support almost all options in `sortablejs` 12 | - support drag from one to another table 13 | - support treeTable 14 | - support vetur 15 | - support onMove 16 | - support drag into an empty `el-table` 17 | 18 | ### You can see in Demos 19 | 20 | - Drag rows 21 | - Drag columns(>1.1.0) 22 | - Drag tree(>1.2.0) 23 | - disable move by set onMove(>1.3.0) 24 | - Set handle for drag 25 | - Set group 26 | - ...other option in sortable.js 27 | - event input, after the change of all 28 | 29 | ## Install 30 | 31 | ### use npm or yarn 32 | 33 | ```bash 34 | yarn add el-table-draggable 35 | 36 | npm i -S el-table-draggable 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```js 42 | import ElTableDraggable from "el-table-draggable"; 43 | 44 | export default { 45 | components: { 46 | ElTableDraggable, 47 | }, 48 | }; 49 | ``` 50 | 51 | ### template 52 | 53 | ```html 54 | 59 | ``` 60 | 61 | ### props 62 | 63 | #### tag 64 | 65 | the wrapper tag of el-table, default is `div` 66 | 67 | #### column 68 | 69 | support drag column 70 | 71 | #### onMove 72 | 73 | set onMove callback 74 | 75 | ```javascript 76 | onMove: function (/**Event*/evt, /**Event*/originalEvent, domInfo) { 77 | // Example: https://jsbin.com/nawahef/edit?js,output 78 | evt.dragged; // dragged HTMLElement 79 | evt.draggedRect; // DOMRect {left, top, right, bottom} 80 | evt.related; // HTMLElement on which have guided 81 | evt.relatedRect; // DOMRect 82 | evt.willInsertAfter; // Boolean that is true if Sortable will insert drag element after target by default 83 | originalEvent.clientY; // mouse position 84 | 85 | domInfo.dragged // the origin dom info of dragged tr, like parent domInfo, level, data, and it's index 86 | domInfo.related // like dragged 87 | 88 | // return false; — for cancel 89 | // return -1; — insert before target 90 | // return 1; — insert after target 91 | }, 92 | ``` 93 | 94 | #### other 95 | 96 | [sortablejs's option](https://github.com/SortableJS/Sortable#options) 97 | 98 | ### Event 99 | 100 | #### input 101 | 102 | data or cloumn after change 103 | 104 | #### other 105 | 106 | [sortablejs's option](https://github.com/SortableJS/Sortable#options), the option start with `on`, Example`onSort => @sort` 107 | 108 | ## todo 109 | 110 | - [ ] Tree Table 111 | 112 | ## Donation 113 | 114 | [By me a coffee](https://buymeacoffee.com/mizukawu) 115 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ], 11 | "types/*": [ 12 | "types/*" 13 | ] 14 | }, 15 | "typeRoots": [ 16 | "node_modules/@types", 17 | "types" 18 | ], 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "el-table-draggable", 3 | "description": "make el-table draggable, support row, column, expanded, sortable.js", 4 | "version": "1.4.12", 5 | "author": { 6 | "name": "mizuka", 7 | "email": "mizuka.wu@outlook.com", 8 | "url": "https://www.mizuka.top" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mizuka-wu/el-table-draggable.git" 13 | }, 14 | "keywords": [ 15 | "element-ui", 16 | "vue", 17 | "vue2", 18 | "sortablejs", 19 | "sortable.js", 20 | "table", 21 | "draggable", 22 | "column", 23 | "el-table", 24 | "expanded-row" 25 | ], 26 | "scripts": { 27 | "serve": "vue-cli-service serve", 28 | "build:demo": "vue-cli-service build --dest examples", 29 | "build": "vue-cli-service build --target lib --name ElTableDraggable src/components/ElTableDraggable.vue", 30 | "lint": "vue-cli-service lint" 31 | }, 32 | "main": "dist/ElTableDraggable.umd.js", 33 | "vetur": { 34 | "tags": "vetur/tags.json", 35 | "attributes": "vetur/attributes.json" 36 | }, 37 | "files": [ 38 | "src/components", 39 | "vetur", 40 | "dist" 41 | ], 42 | "dependencies": { 43 | "core-js": "^3.6.5", 44 | "highlight.js": "^9.18.5", 45 | "lodash": "^4.17.21", 46 | "sortablejs": "^1.14.0", 47 | "vue": "^2.6.11", 48 | "vue-highlight-component": "^1.0.0" 49 | }, 50 | "devDependencies": { 51 | "@types/sortablejs": "^1.10.7", 52 | "@vue/cli-plugin-babel": "~4.5.0", 53 | "@vue/cli-plugin-eslint": "~4.5.0", 54 | "@vue/cli-service": "~4.5.0", 55 | "babel-eslint": "^10.1.0", 56 | "element-ui": "^2.4.5", 57 | "eslint": "^6.7.2", 58 | "eslint-plugin-vue": "^6.2.2", 59 | "mockjs": "^1.1.0", 60 | "vue-router": "^3.5.3", 61 | "vue-template-compiler": "^2.6.11" 62 | }, 63 | "license": "MIT", 64 | "browserslist": [ 65 | "> 1%", 66 | "last 2 versions", 67 | "not dead" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizuka-wu/el-table-draggable/c0c04cf15297ddbd26cef570880738c871943b8f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 83 | 84 | 100 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizuka-wu/el-table-draggable/c0c04cf15297ddbd26cef570880738c871943b8f/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/ElTableDraggable.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 163 | 197 | -------------------------------------------------------------------------------- /src/components/ListViewer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/examples/Base.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /src/examples/Column.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | -------------------------------------------------------------------------------- /src/examples/ColumnWithHandler.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 48 | -------------------------------------------------------------------------------- /src/examples/DataTree.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | -------------------------------------------------------------------------------- /src/examples/DataTreeSampleParent.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 75 | -------------------------------------------------------------------------------- /src/examples/Handle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/examples/MultiTable.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/examples/MultiTableClone.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /src/examples/MultiTableGroup.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /src/examples/MultiTableNestint.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /src/examples/SimpleTableNestint.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import Element from 'element-ui' 4 | import Router from 'vue-router' 5 | import 'element-ui/lib/theme-chalk/index.css' 6 | import ElTableDraggable from './components/ElTableDraggable.vue' 7 | import ListViewer from "./components/ListViewer.vue" 8 | import Highlight from 'vue-highlight-component' 9 | // import hljs from 'highlight.js' 10 | import 'highlight.js/styles/codepen-embed.css' 11 | 12 | // hljs.registerLanguage('html', import('highlight.js/lib/languages/htmlbars')) 13 | 14 | Vue.use(Element) 15 | Vue.use(Router) 16 | Vue.component("CodeViewer", Highlight) 17 | Vue.component("ListViewer", ListViewer) 18 | Vue.component("ElTableDraggable", ElTableDraggable) 19 | 20 | Vue.config.productionTip = false 21 | 22 | new Vue({ 23 | router: new Router({ 24 | routes: [] 25 | }), 26 | render: h => h(App), 27 | }).$mount('#app') 28 | -------------------------------------------------------------------------------- /src/utils/createTable.js: -------------------------------------------------------------------------------- 1 | import Mock from "mockjs" 2 | export const columns = [ 3 | { 4 | key: "index", 5 | type: "number", 6 | width: 80, 7 | }, 8 | { 9 | key: "id", 10 | type: "guid", 11 | width: 160 12 | }, 13 | { 14 | key: "name", 15 | type: "name", 16 | width: 300 17 | }, 18 | { 19 | key: "url", 20 | type: "url", 21 | }, 22 | ] 23 | 24 | const mockTemplate = Object.fromEntries(columns.map(({ key, type }) => [key, `@${type}`])) 25 | 26 | export function createData(total = 5) { 27 | return Array.from(new Array(total)).map((key, index) => { 28 | return { 29 | ...Mock.mock(mockTemplate), 30 | index 31 | } 32 | }) 33 | } -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import throttle from "lodash/throttle"; 3 | import Sortable from "sortablejs"; 4 | const { utils } = Sortable; 5 | const { css } = utils; 6 | 7 | /** @type {Set} */ 8 | const animatedSet = new Set(); 9 | 10 | const LEVEL_REGEXP = /--level-(\d+)/; 11 | export const EMPTY_FIX_CSS = "el-table-draggable__empty-table"; 12 | export const ANIMATED_CSS = "el-table-draggable__animated"; 13 | export const INDENT_CSS = "el-table__indent"; 14 | export const INDENT_PLACEHOLDER_CSS = 'el-table__placeholder' // 子节点的跟随函数 15 | export const CUSTOMER_INDENT_PLACEHOLDER_CSS = 'el-table-draggable__indent-placeholder' // 子节点的跟随函数 16 | export const CUSTOMER_INDENT_CSS = "el-table-draggable__indent"; 17 | export const PLACEHOLDER_CSS = 'draggable-dominfo-placeholder' 18 | const translateRegexp = /translate\((?.*)px,\s?(?.*)px\)/; 19 | const elTableColumnRegexp = /el-table_\d*_column_\d*/; 20 | 21 | /** 22 | * 根据行名称获取当前的层级 23 | * @param {string} className 24 | * @return {number} 25 | */ 26 | export function getLevelFromClassName(className) { 27 | const level = (LEVEL_REGEXP.exec(className) || [])[1] || 0; 28 | return +(level || 0); 29 | } 30 | 31 | /** 32 | * 获取class 33 | * @param {number} level 34 | * @returns {string} 35 | */ 36 | export function getLevelRowClassName(level) { 37 | return `el-table__row--level-${level}`; 38 | } 39 | 40 | /** 41 | * 修改某个dom的className 42 | * @param {Element} tr 43 | * @param {number} [targetLevel] 44 | */ 45 | export function changeRowLevel(tr, targetLevel = 0) { 46 | const sourceLevel = getLevelFromClassName(tr.className); 47 | if (sourceLevel === targetLevel) { 48 | return; 49 | } 50 | 51 | const sourceClassName = getLevelRowClassName(sourceLevel); 52 | const targetClassName = getLevelRowClassName(targetLevel); 53 | tr.classList.remove(sourceClassName); 54 | tr.classList.add(targetClassName); 55 | } 56 | 57 | /** 58 | * 清除造成的所有的副作用 59 | */ 60 | export function cleanUp() { 61 | /** 62 | * 清除EMPTY 63 | */ 64 | Array.from(document.querySelectorAll(`.${EMPTY_FIX_CSS}`)).forEach((el) => { 65 | el.classList.remove(EMPTY_FIX_CSS); 66 | }); 67 | // 移除动画的css 68 | clearAnimate(); 69 | 70 | const needRemovedElements = [ 71 | // 树的子级占位 72 | ...Array.from(document.querySelectorAll(`.${PLACEHOLDER_CSS}`)), 73 | // 间距的占位 74 | ...Array.from(document.querySelectorAll(`.${CUSTOMER_INDENT_PLACEHOLDER_CSS}`)), 75 | ...Array.from(document.querySelectorAll(`.${CUSTOMER_INDENT_CSS}`)) 76 | ] 77 | setTimeout(() => { 78 | needRemovedElements.forEach(el => { 79 | remove(el) 80 | }) 81 | }) 82 | } 83 | 84 | /** 85 | * 重设transform 86 | * @param {Element} el 87 | */ 88 | function resetTransform(el) { 89 | css(el, "transform", ""); 90 | css(el, "transitionProperty", ""); 91 | css(el, "transitionDuration", ""); 92 | } 93 | 94 | /** 95 | * 获取原始的boundge位置 96 | * @param {Element} el 97 | * @param {boolean} ignoreTranslate 98 | * @returns {DOMRect} 99 | */ 100 | export function getDomPosition(el, ignoreTranslate = true) { 101 | const position = el.getBoundingClientRect().toJSON(); 102 | const transform = el.style.transform; 103 | if (transform && ignoreTranslate) { 104 | const { groups = { x: 0, y: 0 } } = translateRegexp.exec(transform) || {}; 105 | position.x = position.x - +groups.x; 106 | position.y = position.y - +groups.y; 107 | } 108 | return position; 109 | } 110 | 111 | /** 112 | * 添加动画 113 | * @param {Element} el 114 | * @param {string} transform 115 | * @param {number} animate 116 | */ 117 | export function addAnimate(el, transform, animate = 0) { 118 | el.classList.add(ANIMATED_CSS); 119 | css(el, "transitionProperty", `transform`); 120 | css(el, "transitionDuration", animate + "ms"); 121 | css(el, "transform", transform); 122 | animatedSet.add(el); 123 | } 124 | 125 | /** 126 | * 清除除了可忽略选项内的动画 127 | * @param {Element[]|Element} targetList 128 | */ 129 | export function clearAnimate(targetList = []) { 130 | const list = Array.isArray(targetList) ? targetList : [targetList]; 131 | const removedIteratory = list.length ? list : animatedSet.values(); 132 | for (const el of removedIteratory) { 133 | el.classList.remove(ANIMATED_CSS); 134 | resetTransform(el); 135 | if (animatedSet.has(el)) { 136 | animatedSet.delete(el); 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * 获取移动的animate 143 | * @param {Element} el 144 | * @param {{x?: number, y?:number}} target 145 | * @returns {string} 146 | */ 147 | export function getTransform(el, target) { 148 | const currentPostion = getDomPosition(el); 149 | const originPosition = getDomPosition(el, true); 150 | const { x, y } = target; 151 | const toPosition = { 152 | x: x !== undefined ? x : currentPostion.x, 153 | y: y !== undefined ? y : currentPostion.y, 154 | }; 155 | const transform = `translate(${toPosition.x - originPosition.x}px, ${toPosition.y - originPosition.y 156 | }px)`; 157 | return transform; 158 | } 159 | 160 | /** 161 | * 移动到具体位置 162 | * @param {Element} el 163 | * @param {{x?: number, y?:number}} target 164 | * @returns {string} 165 | */ 166 | export function translateTo(el, target) { 167 | resetTransform(el); 168 | const transform = getTransform(el, target); 169 | addAnimate(el, transform) 170 | } 171 | 172 | /** 173 | * 交换 174 | * @param {Element} newNode 175 | * @param {Element} referenceNode 176 | * @param {number} animate 177 | */ 178 | export function insertBefore(newNode, referenceNode, animate = 0) { 179 | /** 180 | * 动画效果 181 | * @todo 如果是不同列表,动画方案更新 182 | */ 183 | if (animate) { 184 | // 同一列表处理 185 | if (newNode.parentNode === referenceNode.parentNode) { 186 | // source 187 | const offset = newNode.offsetTop - referenceNode.offsetTop; 188 | if (offset !== 0) { 189 | const subNodes = Array.from(newNode.parentNode.children); 190 | const indexOfNewNode = subNodes.indexOf(newNode); 191 | const indexOfReferenceNode = subNodes.indexOf(referenceNode); 192 | const nodes = subNodes 193 | .slice( 194 | Math.min(indexOfNewNode, indexOfReferenceNode), 195 | Math.max(indexOfNewNode, indexOfReferenceNode) 196 | ) 197 | .filter((item) => item !== newNode); 198 | const newNodeHeight = 199 | offset > 0 ? -1 * newNode.offsetHeight : newNode.offsetHeight; 200 | nodes.forEach((node) => 201 | addAnimate(node, `translateY(${newNodeHeight}px)`, animate) 202 | ); 203 | addAnimate(newNode, `translateY(${offset}px)`, animate); 204 | } 205 | } else { 206 | console.log("非同一列表"); 207 | } 208 | 209 | // 清除 210 | setTimeout(() => { 211 | clearAnimate(); 212 | }, animate); 213 | } 214 | referenceNode.parentNode.insertBefore(newNode, referenceNode); 215 | } 216 | 217 | /** 218 | * 交换 219 | * @param {Element} newNode 220 | * @param {Element} referenceNode 221 | * @param {number} animate 222 | */ 223 | export function insertAfter(newNode, referenceNode, animate = 0) { 224 | const targetReferenceNode = referenceNode.nextSibling; 225 | insertBefore(newNode, targetReferenceNode, animate); 226 | } 227 | 228 | /** 229 | * 交换元素位置 230 | * @todo 优化定时器 231 | * @param {Element} prevNode 232 | * @param {Element} nextNode 233 | * @param {number} animate 234 | */ 235 | export function exchange(prevNode, nextNode, animate = 0) { 236 | const exchangeList = [ 237 | { 238 | from: prevNode, 239 | to: nextNode, 240 | }, 241 | { 242 | from: nextNode, 243 | to: prevNode, 244 | }, 245 | ]; 246 | exchangeList.forEach(({ from, to }) => { 247 | const targetPosition = getDomPosition(to, false); 248 | 249 | // 宽度需要修正 250 | const { width } = getDomPosition(from, false) 251 | const targetWidth = targetPosition.width 252 | if (width !== targetWidth) { 253 | const offset = width - targetWidth 254 | targetPosition.x = targetPosition.x + offset 255 | } 256 | const transform = getTransform(from, targetPosition); 257 | addAnimate(from, transform, animate); 258 | }); 259 | } 260 | 261 | /** 262 | * @param {Element} el 263 | */ 264 | export function remove(el) { 265 | if (el.parentElement) { 266 | el.parentElement.removeChild(el); 267 | } 268 | } 269 | 270 | /** 271 | * 从th获取对应的td 272 | * @todo 支持跨表格获取tds 273 | * @param {Element} th 274 | * @returns {NodeListOf} 275 | */ 276 | export function getTdListByTh(th) { 277 | const className = Array.from(th.classList).find((className) => 278 | elTableColumnRegexp.test(className) 279 | ); 280 | return document.querySelectorAll(`td.${className}`); 281 | } 282 | 283 | /** 284 | * 285 | * @param {Element} th 286 | * @returns {string} 287 | */ 288 | export function getColName(th) { 289 | const className = Array.from(th.classList).find((className) => 290 | elTableColumnRegexp.test(className) 291 | ); 292 | return className 293 | } 294 | 295 | /** 296 | * 从th获取对应的col 297 | * @todo 支持跨表格获取tds 298 | * @param {Element} th 299 | * @returns {Element} 300 | */ 301 | export function getColByTh(th) { 302 | const className = getColName(th) 303 | return document.querySelector(`[name=${className}]`); 304 | } 305 | 306 | /** 307 | * 自动对齐列 308 | * @param {Element[]|Element} thList 309 | */ 310 | export const alignmentTableByThList = throttle(function alignmentTableByThList( 311 | thList 312 | ) { 313 | const list = Array.isArray(thList) ? thList : [thList]; 314 | list.forEach((th) => { 315 | const tdList = getTdListByTh(th); 316 | tdList.forEach((td) => { 317 | const { x } = getDomPosition(th); 318 | translateTo(td, { x }); 319 | }); 320 | }); 321 | }, 322 | 1000 / 120); 323 | 324 | /** 325 | * 切换row的打开还是关闭 326 | * @param {import('./options.js').DomInfo} domInfo 327 | * @param {boolean} expanded 是否收起 328 | */ 329 | export function toggleExpansion(domInfo, expanded = true) { 330 | // 插入排序需要倒序插入 331 | domInfo.childrenList 332 | .slice() 333 | .reverse() 334 | .forEach((childrenDomInfo) => { 335 | if (expanded) { 336 | // 展开的话,需要显示,并修正位置和indent 337 | const originDisplay = 338 | childrenDomInfo.isShow ? null : childrenDomInfo.el.style.display; 339 | childrenDomInfo.el.style.display = originDisplay; 340 | insertAfter(childrenDomInfo.el, domInfo.el); 341 | changeDomInfoLevel(childrenDomInfo, childrenDomInfo.level); 342 | } else { 343 | childrenDomInfo.el.style.display = "none"; 344 | } 345 | 346 | /** 347 | * 处理子节点 348 | */ 349 | if (childrenDomInfo.childrenList.length) { 350 | toggleExpansion(childrenDomInfo, expanded); 351 | } 352 | }); 353 | } 354 | 355 | /** 356 | * 切换某个domInfo的锁进 357 | * @param {import('./options.js').DomInfo} domInfo 358 | * @param {number} level 359 | * @param {number} indent 360 | * @param {boolean} showPlaceholder 是否显示expanded的占位 361 | */ 362 | export function changeDomInfoLevel(domInfo, level = 0, indent = 16) { 363 | const { el } = domInfo; 364 | domInfo.level = level; 365 | changeRowLevel(el, level); 366 | const cell = el.querySelector("td:nth-child(1) .cell"); 367 | if (!cell) { 368 | return; 369 | } 370 | // 判断是否拥有indent 371 | const targetIndent = level * indent; 372 | let indentWrapper = cell.querySelector(`.${INDENT_CSS}`); 373 | if (!indentWrapper) { 374 | indentWrapper = document.createElement("span"); 375 | indentWrapper.classList.add(INDENT_CSS, CUSTOMER_INDENT_CSS); 376 | insertBefore(indentWrapper, cell.firstChild); 377 | } 378 | indentWrapper.style.paddingLeft = `${targetIndent}px`; 379 | domInfo.childrenList.forEach((children) => { 380 | changeDomInfoLevel(children, level + 1, indent); 381 | }); 382 | 383 | let placeholderEl = cell.querySelector(`.${INDENT_PLACEHOLDER_CSS}`); 384 | if (!placeholderEl) { 385 | placeholderEl = document.createElement('span') 386 | placeholderEl.classList.add(INDENT_PLACEHOLDER_CSS, CUSTOMER_INDENT_PLACEHOLDER_CSS) 387 | insertAfter(placeholderEl, indentWrapper) 388 | } 389 | placeholderEl.style.display = targetIndent ? null : 'none'; 390 | } 391 | 392 | /** 393 | * 交换两个dom的位置 394 | * @param {Element} a 395 | * @param {Element} b 396 | */ 397 | export function swapDom(a, b) { 398 | const p1 = a.parentNode 399 | const p2 = b.parentNode 400 | let sib = b.nextSibling; 401 | if (sib === a) { 402 | sib = sib.nextSibling 403 | } 404 | p1.replaceChild(b, a); 405 | if (sib) { 406 | p2.insertBefore(a, sib) 407 | } else { 408 | p2.appendChild(a) 409 | } 410 | return true; 411 | } 412 | 413 | export default { 414 | alignmentTableByThList, 415 | getTransform, 416 | clearAnimate, 417 | addAnimate, 418 | ANIMATED_CSS, 419 | getTdListByTh, 420 | translateTo, 421 | getDomPosition, 422 | insertAfter, 423 | insertBefore, 424 | remove, 425 | exchange, 426 | cleanUp, 427 | toggleExpansion, 428 | changeDomInfoLevel, 429 | getLevelFromClassName, 430 | getLevelRowClassName, 431 | getColByTh, 432 | getColName, 433 | swapDom 434 | }; 435 | -------------------------------------------------------------------------------- /src/utils/manualRowExchange.js: -------------------------------------------------------------------------------- 1 | // 以下是交换行相关的操作 2 | /** 3 | * 模拟的效果 4 | */ 5 | function mockExchange (index, targetIndex) { 6 | if (!this.sortable) { 7 | return 8 | } 9 | const rows = this.$el.querySelectorAll('.el-table__row') 10 | const orientation = index > targetIndex ? 1 : -1 // 方向参数 1 为向上 -1 为向下 11 | const offset = orientation * rows[index].offsetHeight // 单次位移的距离 12 | 13 | // rows增加补间动画 14 | rows.forEach(row => { 15 | row.style.transition = 'transform 500ms' 16 | }) 17 | 18 | // 生成受影响的index 19 | let effectedRows = [index, targetIndex].sort(function (i, j) { 20 | return i > j ? 1 : -1 21 | }) 22 | // 最大最小 然后补全中间的函数 23 | const [min, max] = effectedRows 24 | for (let effectedIndex = min + 1; effectedIndex < max; effectedIndex++) { 25 | effectedRows.push(effectedIndex) 26 | } 27 | 28 | // 排除掉index本身并排序(这玩意比较特殊)转为row的列表 29 | effectedRows = effectedRows 30 | .filter(effectedIndex => effectedIndex !== index) 31 | .sort(function (i, j) { 32 | return i > j ? 1 : -1 33 | }) 34 | .map(index => rows[index]) 35 | 36 | const effectedRowOffset = `${offset}px` 37 | effectedRows.forEach(row => { 38 | row.style.transform = `translateY(${effectedRowOffset})` 39 | }) 40 | 41 | // 主体的动画部分 动画结束之后再刷新页面 42 | const originRow = rows[index] 43 | 44 | // 移动那些受影响的行的高度 45 | const originRowOffset = effectedRows.reduce( 46 | (total, row) => (total += row.offsetHeight), 47 | 0 48 | ) 49 | originRow.style.transform = `translateY(${orientation * 50 | -1 * 51 | originRowOffset}px)` 52 | originRow.style.zIndex = 9999 53 | originRow.style.boxShadow = '2px 2px 5px #eeeeee' 54 | setTimeout(() => { 55 | originRow.style.zIndex = '' 56 | originRow.style.boxShadow = '' 57 | this.exchangeRow(index, targetIndex, false) 58 | }, 500) 59 | }, 60 | /** 61 | * 交换行 62 | * @param {number} index - 原来的index 63 | * @param {number} targetIndex - 目标的index 64 | * @param {boolean} mock - 是否模拟排序?只放出动画 65 | */ 66 | function exchangeRow (index, targetIndex, mock = false) { 67 | if (!this.sortable) { 68 | return 69 | } 70 | // 一些不能交换的状态 71 | if (targetIndex < 0) { 72 | return 73 | } 74 | if (index === targetIndex) { 75 | return 76 | } 77 | if (targetIndex > this.data.length) { 78 | return 79 | } 80 | 81 | // 如果是mock 模拟交换动画 82 | if (mock) { 83 | this.mockExchange(index, targetIndex) 84 | } else { 85 | const array = this.data 86 | const target = array[index] 87 | 88 | const rows = this.$el.querySelectorAll('.el-table__row') 89 | rows.forEach(row => { 90 | row.style.transition = '' 91 | row.style.transform = '' 92 | }) 93 | 94 | const toAfter = targetIndex > index 95 | // 插入 96 | this.data.splice(toAfter ? targetIndex + 1 : targetIndex, 0, target) 97 | // 删除(插入到之前的话,原始index 需要 + 1) 98 | this.data.splice(toAfter ? index : index + 1, 1) 99 | this.$emit('exchange', array) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/options/column.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unreachable */ 2 | /* eslint-disable no-unused-vars */ 3 | /** 4 | * 根据不同类型使用不同的option 5 | */ 6 | import dom from "@/utils/dom"; 7 | import { 8 | updateElTableInstance, 9 | getOnMove, 10 | exchange, 11 | } from "@/utils/utils"; 12 | 13 | export const WRAPPER = ".el-table__header-wrapper thead tr" 14 | export const DRAGGABLE = "th" 15 | 16 | /** 17 | * 行列的基础config 18 | */ 19 | export const config = { 20 | WRAPPER: ".el-table__header-wrapper thead tr", 21 | DRAGGABLE: "th", 22 | /** 23 | * @param {Map} context 24 | * @param {Vue} elTableInstance 25 | * @param {number} animation 26 | * @returns {import('@types/sortablejs').SortableOptions} 27 | */ 28 | OPTION(context, elTableInstance, animation) { 29 | let isDragging = false // 正在拖拽 30 | let columnIsMoving = false // 列正在移动 31 | // 自动对齐 32 | function autoAlignmentTableByThList(thList) { 33 | if (!isDragging) { 34 | return 35 | } 36 | dom.alignmentTableByThList(thList) 37 | return requestAnimationFrame(() => { 38 | autoAlignmentTableByThList(thList) 39 | }) 40 | } 41 | 42 | /** 列宽的虚拟dom */ 43 | let colDomInfoList = [] 44 | 45 | return { 46 | onStart() { 47 | const thList = Array.from(elTableInstance.$el.querySelector(WRAPPER).childNodes) 48 | 49 | colDomInfoList = thList.map(th => { 50 | const col = dom.getColByTh(th) 51 | const width = col ? col.getAttribute('width') : th.offsetWidth 52 | return { 53 | el: col, 54 | thEl: th, 55 | width: width, 56 | originWidth: width 57 | } 58 | }) 59 | 60 | // dragging状态自动调用对齐 61 | isDragging = true 62 | autoAlignmentTableByThList(thList) 63 | }, 64 | setData(dataTransfer, dragEl) { 65 | /** 66 | * 在页面上创建一个当前table的wrapper,然后隐藏它,只显示那一列的部分作为拖拽对象 67 | * 在下一个事件循环删除dom即可 68 | */ 69 | const { offsetLeft, offsetWidth, offsetHeight } = dragEl; 70 | const tableEl = elTableInstance.$el; 71 | 72 | const wrapper = document.createElement("div"); // 可视区域 73 | wrapper.style = `position: fixed; z-index: -1;overflow: hidden; width: ${offsetWidth}px`; 74 | const tableCloneWrapper = document.createElement("div"); // table容器,宽度和位移 75 | tableCloneWrapper.style = `position: relative; left: -${offsetLeft}px; width: ${tableEl.offsetWidth}px`; 76 | wrapper.appendChild(tableCloneWrapper); 77 | tableCloneWrapper.appendChild(tableEl.cloneNode(true)); 78 | 79 | // 推进dom,让dataTransfer可以获取 80 | document.body.appendChild(wrapper); 81 | // 拖拽位置需要偏移到对应的列上 82 | dataTransfer.setDragImage( 83 | wrapper, 84 | (offsetLeft * 2) + (offsetWidth / 2), 85 | offsetHeight / 2 86 | ); 87 | setTimeout(() => { 88 | document.body.removeChild(wrapper); 89 | }); 90 | }, 91 | onMove(evt, originEvent) { 92 | const { related, dragged, relatedRect, draggedRect } = evt; 93 | let { willInsertAfter } = evt; 94 | 95 | // 根据用户选择 96 | const onMove = getOnMove(elTableInstance); 97 | const onMoveResult = onMove(evt, originEvent) 98 | switch (onMoveResult) { 99 | case 1: { 100 | willInsertAfter = true; 101 | break; 102 | } 103 | case -1: { 104 | willInsertAfter = false; 105 | break; 106 | } 107 | case false: { 108 | return false; 109 | } 110 | default: { 111 | break; 112 | } 113 | } 114 | 115 | /** 116 | * 对dom进行操作 117 | */ 118 | const thList = willInsertAfter ? [dragged, related] : [related, dragged]; 119 | // 临时修改两个的宽度, 需要在下个循环触发,省的宽度不一致导致因为dom变化再次触发拖拽 120 | const colList = thList 121 | .map(th => colDomInfoList.find(item => item.thEl === th)) 122 | // 交换宽度 123 | if (colList.length !== 2) { 124 | throw new Error('无法找到拖拽的th的信息,请检查是否跨表格拖拽了') 125 | return true 126 | } 127 | const [fromCol, toCol] = colList 128 | setTimeout(() => { 129 | dom.swapDom(fromCol.el, toCol.el) 130 | // 交换colDomInfoList内位置 131 | const oldIndex = colDomInfoList.indexOf(fromCol) 132 | const newIndex = colDomInfoList.indexOf(toCol) 133 | exchange(oldIndex, colDomInfoList, newIndex, colDomInfoList) 134 | }) 135 | 136 | return true; 137 | }, 138 | onEnd(evt) { 139 | const PROP = "columns"; 140 | dom.cleanUp(); 141 | // 清除所有临时交换产生的设定和变量 142 | colDomInfoList.forEach(({ el, originWidth }) => { 143 | if (el) { 144 | el.setAttribute('width', originWidth) 145 | } 146 | }) 147 | 148 | isDragging = false 149 | 150 | const { to, from, pullMode } = evt; 151 | const toContext = context.get(to); 152 | let toList = toContext[PROP]; 153 | const fromContext = context.get(from); 154 | let fromList = fromContext[PROP]; 155 | let { newIndex, oldIndex } = evt; 156 | 157 | // 交换dom位置 158 | exchange(oldIndex, fromList, newIndex, toList, pullMode); 159 | 160 | // 交换传递下来的column的value 161 | const fromValue = fromContext.$parent.value || []; 162 | const toValue = toContext.$parent.value || []; 163 | if (fromValue.length && toValue.length) { 164 | exchange(oldIndex, fromValue, newIndex, toValue, pullMode); 165 | } 166 | 167 | // 通知对应的实例 168 | updateElTableInstance(from, to, context, function (tableContext) { 169 | const draggableContext = tableContext.$parent; 170 | const columns = draggableContext.value 171 | ? draggableContext.value 172 | : tableContext[PROP].map(({ property }) => ({ property })); 173 | draggableContext.$emit("input", columns); 174 | }); 175 | }, 176 | }; 177 | }, 178 | }; 179 | 180 | export default config 181 | -------------------------------------------------------------------------------- /src/utils/options/constant.js: -------------------------------------------------------------------------------- 1 | export const DOM_MAPPING_NAME = "_mapping"; -------------------------------------------------------------------------------- /src/utils/options/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unreachable */ 2 | /* eslint-disable no-unused-vars */ 3 | /** 4 | * 根据不同类型使用不同的option 5 | */ 6 | import rowConfig from './row' 7 | import columnConfig from './column' 8 | 9 | export { DOM_MAPPING_NAME } from './constant' 10 | 11 | /** 12 | * 行列的基础config 13 | */ 14 | export const CONFIG = { 15 | ROW: rowConfig, 16 | COLUMN: columnConfig, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/options/row.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unreachable */ 2 | /* eslint-disable no-unused-vars */ 3 | import { DOM_MAPPING_NAME } from './constant' 4 | import { 5 | // fixDomInfoByDirection, 6 | MappingOberver, 7 | getOnMove, 8 | exchange, 9 | updateElTableInstance, 10 | checkIsTreeTable, 11 | addTreePlaceholderRows 12 | } from "@/utils/utils"; 13 | import dom, { 14 | cleanUp, 15 | EMPTY_FIX_CSS, 16 | } from "@/utils/dom"; 17 | 18 | export const WRAPPER = '.el-table__body-wrapper tbody' 19 | export const DRAGGABLE = '.el-table__row' 20 | 21 | export const config = { 22 | WRAPPER, 23 | DRAGGABLE, 24 | /** 25 | * @param {Map} context 26 | * @param {Vue} elTableInstance 27 | * @param {number} animation 28 | * @returns {import('@types/sortablejs').SortableOptions} 29 | */ 30 | OPTION(context, elTableInstance, animation) { 31 | const PROP = 'data' 32 | /** 33 | * 自动监听重建映射表 34 | */ 35 | if (elTableInstance[DOM_MAPPING_NAME]) { 36 | elTableInstance[DOM_MAPPING_NAME].stop(); 37 | } 38 | const mappingOberver = new MappingOberver( 39 | elTableInstance, 40 | WRAPPER, 41 | ); 42 | 43 | elTableInstance[DOM_MAPPING_NAME] = mappingOberver; 44 | mappingOberver.rebuild(); 45 | mappingOberver.start(); 46 | let dommappingTimer = null 47 | let isTreeTable = checkIsTreeTable(elTableInstance) // 防止因为数据原因变化,所以每次选择都判断一次 48 | 49 | return { 50 | onChoose() { 51 | isTreeTable = checkIsTreeTable(elTableInstance) 52 | cleanUp() 53 | /** 54 | * 开始之前对所有表格做一定处理 55 | */ 56 | for (const draggableTable of context.values()) { 57 | const domMapping = draggableTable[DOM_MAPPING_NAME]; 58 | // 暂停dom监听,防止拖拽变化不停触发 59 | if (domMapping) { 60 | domMapping.stop(); 61 | } 62 | 63 | if (isTreeTable) { 64 | addTreePlaceholderRows( 65 | domMapping.mapping, 66 | elTableInstance.treeProps, 67 | DRAGGABLE.replace('.', '')) 68 | } 69 | 70 | /** 71 | * 解决手动关闭后会有的错位问题 72 | * 导致原因,default-expanded-all 73 | * 需要记录一下当前打开的行,结束之后还原状态(待定) 74 | */ 75 | draggableTable.store.states.defaultExpandAll = false; 76 | 77 | // 如果是空表格,增加一个css 78 | const tableEl = draggableTable.$el.querySelector( 79 | ".el-table__body-wrapper table" 80 | ); 81 | if (tableEl.clientHeight === 0) { 82 | // body-wrapper增加样式,让overflw可显示同时table有个透明区域可拖动 83 | tableEl.parentNode.classList.add(EMPTY_FIX_CSS); 84 | } 85 | } 86 | }, 87 | onStart(evt) { 88 | /** 89 | * expanded/树表格的处理, 关闭展开行 90 | */ 91 | const { item } = evt; 92 | const domInfo = mappingOberver.mapping.get(item); 93 | // 收起拖动的行的已展开行 94 | dom.toggleExpansion(domInfo, false); 95 | }, 96 | onMove(evt, originEvt) { 97 | const { related, dragged, to, from, willInsertAfter } = evt; 98 | const fromContext = context.get(from); 99 | const toContext = context.get(to); 100 | /** @type {DomInfo} */ 101 | const draggedDomInfo = 102 | fromContext[DOM_MAPPING_NAME].mapping.get(dragged); 103 | /** @type {DomInfo} */ 104 | const relatedDomInfo = 105 | toContext[DOM_MAPPING_NAME].mapping.get(related); 106 | 107 | /** 108 | * 树状表格的特殊处理,如果碰到的dom不是placeholder,则无视 109 | */ 110 | if (isTreeTable) { 111 | if (relatedDomInfo.type !== 'placeholder') { 112 | return false 113 | } 114 | } 115 | 116 | /** 117 | * 判断是否需要修正当前dragged的对应level 118 | */ 119 | // let targrtDomInfo = fixDomInfoByDirection( 120 | // relatedDomInfo, 121 | // draggedDomInfo, 122 | // willInsertAfter 123 | // ); 124 | let targrtDomInfo = relatedDomInfo 125 | 126 | const onMove = getOnMove(elTableInstance); 127 | if (onMove) { 128 | const onMoveResutl = onMove(evt, originEvt, { 129 | dragged: draggedDomInfo, 130 | related: targrtDomInfo, 131 | }); 132 | 133 | /** 134 | * @todo 兼容willInserAfter属性 135 | */ 136 | switch (onMoveResutl) { 137 | case 1: { 138 | if (!willInsertAfter) { 139 | targrtDomInfo = relatedDomInfo 140 | // fixDomInfoByDirection( 141 | // relatedDomInfo, 142 | // draggedDomInfo, 143 | // true 144 | // ); 145 | } 146 | break; 147 | } 148 | case -1: { 149 | if (willInsertAfter) { 150 | targrtDomInfo = relatedDomInfo 151 | // fixDomInfoByDirection( 152 | // relatedDomInfo, 153 | // draggedDomInfo, 154 | // false 155 | // ); 156 | } 157 | break; 158 | } 159 | case false: { 160 | return false; 161 | } 162 | default: { 163 | break; 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * relatedDomInfo,自动将children插入到自身后方 170 | * @todo 需要增加动画效果,目标直接插入,需要在下一循环,位置变化好后再配置 171 | */ 172 | setTimeout(() => { 173 | /** @type {import('types/DomInfo').DomInfo} */ 174 | relatedDomInfo.childrenList.forEach((children) => { 175 | // expanded或者是影子行 176 | if (children.type === "proxy") { 177 | dom.insertAfter(children.el, relatedDomInfo.el); 178 | } 179 | }); 180 | }); 181 | 182 | const { 183 | states: { indent }, 184 | } = fromContext.store; 185 | dom.changeDomInfoLevel(draggedDomInfo, targrtDomInfo.level, indent); 186 | }, 187 | onEnd(evt) { 188 | const { to, from, pullMode, newIndex, item, oldIndex } = evt; 189 | const fromContext = context.get(from); 190 | const toContext = context.get(to); 191 | 192 | /** @type {DomInfo} */ 193 | const fromDomInfo = fromContext[DOM_MAPPING_NAME].mapping.get(item); 194 | /** 195 | * @type {DomInfo[]} 196 | * 之前目标位置的dom元素, 因为dom已经换了,所以需要通过elIndex的方式重新找回来 197 | */ 198 | const toDomInfoList = Array.from( 199 | toContext[DOM_MAPPING_NAME].mapping.values() 200 | ); 201 | const toDomInfo = 202 | toDomInfoList.find((domInfo) => domInfo.elIndex === newIndex) || 203 | toContext[DOM_MAPPING_NAME].mapping.get(to); 204 | // const toDomInfo = { 205 | // ...fixDomInfoByDirection( 206 | // originToDomInfo, 207 | // fromDomInfo, 208 | // from === to ? newIndex > oldIndex : false 209 | // ), 210 | // }; 211 | 212 | // 跨表格index修正 213 | if ( 214 | from !== to && 215 | to.querySelectorAll(DRAGGABLE).length <= 2 216 | ) { 217 | toDomInfo.index = newIndex; 218 | } 219 | 220 | /** 221 | * 数据层面的交换 222 | */ 223 | // mapping层面的交换 224 | exchange( 225 | fromDomInfo.index, 226 | fromDomInfo.parent.childrenList, 227 | toDomInfo.index, 228 | toDomInfo.type === "root" 229 | ? toDomInfo.childrenList 230 | : toDomInfo.parent.childrenList, 231 | pullMode 232 | ); 233 | 234 | // 数据层面的交换 235 | exchange( 236 | fromDomInfo.index, 237 | fromDomInfo.data, 238 | toDomInfo.index, 239 | toDomInfo.data, 240 | pullMode 241 | ); 242 | 243 | // clone对象的话,需要从dom层面删除,防止el-table重复渲染 244 | if (pullMode === 'clone' && from !== to) { 245 | to.removeChild(fromDomInfo.el) 246 | } 247 | 248 | // 通知更新 249 | updateElTableInstance(from, to, context, function (tableContext) { 250 | const draggableContext = tableContext.$parent; // 包裹组件 251 | const data = tableContext[PROP]; 252 | draggableContext.$emit("input", data); 253 | }); 254 | 255 | /** 256 | * dom修正,因为exchange之后el-table可能会错乱,所以需要修正位置 257 | * 将原始的dom信息带回来children带回来 258 | * 删除一些临时加进去的行 259 | */ 260 | // 根据mapping自动重新绘制, 最高一层就不用rebuild了 261 | if (toDomInfo.parent && toDomInfo.parent.parent) { 262 | dom.toggleExpansion(toDomInfo.parent, true); 263 | } 264 | // expanded部分 265 | dom.toggleExpansion(fromDomInfo, true); 266 | /** @todo 缓存是否强制expanded */ 267 | toContext.toggleRowExpansion(fromDomInfo.data, true); 268 | 269 | cleanUp() 270 | }, 271 | onUnchoose() { 272 | cleanUp() 273 | /** 274 | * 全局重新开始监听dom变化 275 | * 需要在之前dom操作完成之后进行 276 | */ 277 | if (dommappingTimer) { 278 | clearTimeout(dommappingTimer) 279 | } 280 | dommappingTimer = setTimeout(() => { 281 | for (const draggableTable of context.values()) { 282 | const domMapping = draggableTable[DOM_MAPPING_NAME]; 283 | if (domMapping) { 284 | domMapping.rebuild(); 285 | domMapping.start(); 286 | } 287 | } 288 | }, 100); 289 | }, 290 | }; 291 | }, 292 | } 293 | 294 | export default config -------------------------------------------------------------------------------- /src/utils/ua.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | const ua = navigator.userAgent, 3 | isWindowsPhone = /(?:Windows Phone)/.test(ua), 4 | isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone, 5 | isAndroid = /(?:Android)/.test(ua), 6 | isFireFox = /(?:Firefox)/.test(ua), 7 | isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox && /(?:Tablet)/.test(ua)), 8 | isPhone = /(?:iPhone)/.test(ua) && !isTablet, 9 | isPc = !isPhone && !isAndroid && !isSymbian; 10 | return { 11 | isTablet: isTablet, 12 | isPhone: isPhone, 13 | isAndroid : isAndroid, 14 | isPc : isPc 15 | }; 16 | } -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { getLevelFromClassName, PLACEHOLDER_CSS, insertAfter, insertBefore } from "./dom"; 3 | 4 | /** 5 | * 判断当前表格是否已经树状展开了 6 | * @param {Vue} tableInstance 7 | * @returns {boolean} 8 | */ 9 | export function checkIsTreeTable(tableInstance) { 10 | return Object.keys(tableInstance.store.normalizedData || {}).length > 0; 11 | } 12 | 13 | /** 14 | * 如果是树表格,插入占位行 15 | * @param {import('types/DomInfo').DomMapping} mapping 16 | * @param {{ children: string, hasChildren: string }} treeProps 17 | * @param {string} className 18 | */ 19 | export function addTreePlaceholderRows(mapping, treeProps, className = '') { 20 | const domInfoList = Array.from(mapping.values()) 21 | const root = domInfoList.find(domInfo => domInfo.type === 'root') 22 | 23 | if (!root) { 24 | return 25 | } 26 | 27 | const trDomInfoList = domInfoList.filter(item => item.type === 'common') 28 | trDomInfoList.forEach(trDomInfo => { 29 | /** 30 | * 有children的自动添加一个children占位空间 31 | */ 32 | const data = trDomInfo.data[trDomInfo.index] 33 | if (!data) { 34 | console.log('这个有问题', trDomInfo, trDomInfo.index, trDomInfo.data) 35 | } 36 | const hasChildren = data[treeProps.hasChildren] !== false && data[treeProps.children] 37 | 38 | // 插入 上 hasChildren 下三种占位 39 | const prevPlaceholderEl = document.createElement('tr') 40 | prevPlaceholderEl.classList.add(PLACEHOLDER_CSS, "prev", className) 41 | 42 | // if (needAddPlaceholder) { 43 | // const childPlaceholderEl = document.createElement('tr') 44 | // childPlaceholderEl.classList.add(PLACEHOLDER_CSS, className) 45 | // const latestChildDomInfo = trDomInfo.childrenList[trDomInfo.childrenList.length - 1] 46 | // insertAfter( 47 | // childPlaceholderEl, 48 | // // 如果没有孩子就插入在自身后面 49 | // (latestChildDomInfo || trDomInfo).el 50 | // ) 51 | // /** @type {import('types/DomInfo').DomInfo} */ 52 | // const placeholderDomInfo = { 53 | // el: childPlaceholderEl, 54 | // elIndex: -1, 55 | // level: trDomInfo.level + 1, 56 | // data: data[treeProps.children] || [], 57 | // index: trDomInfo.data.length, // 最后一位 58 | // parent: trDomInfo, 59 | // childrenList: [], 60 | // isShow: true, 61 | // type: 'placeholder', 62 | // } 63 | // mapping.set(childPlaceholderEl, placeholderDomInfo) 64 | // } 65 | 66 | }) 67 | // elIndex重写, 保证获取到对的domInfo 68 | const tbody = root.el 69 | const trList = Array.from(tbody.childNodes) 70 | Array.from(mapping.values()).forEach(domInfo => { 71 | if ('elIndex' in domInfo) { 72 | domInfo.elIndex = trList.indexOf(domInfo.el) 73 | } 74 | }) 75 | } 76 | 77 | /** 78 | * 获取onMove方法 79 | * @param {Vue} tableInstance 80 | * @returns {(evt: Sortable.MoveEvent, originalEvent: Event) => boolean | void | 1 | -1} 81 | */ 82 | export function getOnMove(tableInstance) { 83 | const { 84 | $props: { onMove }, 85 | } = tableInstance.$parent; 86 | return onMove || (() => true); 87 | } 88 | 89 | /** 90 | * 判断是否可见 91 | * @param {Element} el 92 | * @returns {boolean} 93 | */ 94 | export function isVisible(el) { 95 | return window.getComputedStyle(el).display !== "none"; 96 | } 97 | 98 | /** 99 | * 根据方向矫正domInfo 100 | * 因为多级结构的问题,跨层级需要进行一个修正 101 | * 例如1,2,3结构,如果2有2-1的话,拖动到2的情况下 102 | * 其实是希望能够插入到2-1上前 103 | * 所以实际上需要进行一层index的重新计算,其最末尾一个才是真的index 104 | * @param {import('./options').DomInfo} domInfo 目标节点 105 | * @param {import('./options').DomInfo} originDomInfo 原始正在拖拽的 106 | * @param {boolean} willInsertAfter 107 | * @returns {import('types/DomInfo').DomInfo} 108 | */ 109 | // export function fixDomInfoByDirection(domInfo, originDomInfo) { 110 | // // if (!willInsertAfter) { 111 | // // return domInfo; 112 | // // } 113 | // const { childrenList } = domInfo; 114 | // const visibleChildrenList = childrenList.filter((item) => isVisible(item.el)); 115 | // // 某个行的根节点上 116 | // if (visibleChildrenList.length > 0) { 117 | // return visibleChildrenList[0]; 118 | // } 119 | // // 子节点上 120 | // else if (domInfo.level > 0) { 121 | // const { index } = domInfo; 122 | // const { childrenList } = domInfo.parent; 123 | 124 | // // 如果是跨数据层面拖拽,同样需要+1 125 | // const offset = childrenList.includes(originDomInfo) ? 0 : 1; 126 | // const list = childrenList.slice(0).map((item) => ({ 127 | // ...item, 128 | // index: item.index + offset, 129 | // })); 130 | // return list[index]; 131 | // } 132 | // return domInfo; 133 | // } 134 | 135 | /** 136 | * 获取最近一个同级的 137 | * @param {import('./options').DomInfo} domInfo 138 | * @param {number} [targetLevel] 139 | * @returns {import('./options').DomInfo | null} 140 | */ 141 | export function getSameLevelParentDomInfo(domInfo, targetLevel = 0) { 142 | const { level, parent } = domInfo; 143 | 144 | if (level === targetLevel) { 145 | return domInfo; 146 | } 147 | 148 | if (!parent) { 149 | return null; 150 | } 151 | 152 | return getSameLevelParentDomInfo(parent, targetLevel); 153 | } 154 | 155 | /** 156 | * 根据类型当前的dom结构,自动构建每个tr的对应数据关系 157 | * 如果是树状表格,需要增加一个placeholder结构进去 158 | * @param {Vue} tableInstance ElTable实例 159 | * @param {Map} [mapping] 160 | * @param {string} [wrapper] 容器css 161 | * @param {MappingOberver|null} [observer] 162 | * @returns {Map} 163 | */ 164 | export function createOrUpdateDomMapping( 165 | tableInstance, 166 | mapping = new Map(), 167 | wrapper = "", 168 | observer = null 169 | ) { 170 | // table的配置 171 | const { data, treeProps } = tableInstance; 172 | const { children = null } = treeProps || {}; 173 | mapping.clear(); 174 | observer && observer.stop(); // 停止监听变化,构建完成后继续监听 175 | const wrapperEl = tableInstance.$el.querySelector(wrapper); 176 | 177 | /** @type {DomInfo} 最新被使用的dom, 默认是采用了整个table作为root */ 178 | let latestDomInfo = { 179 | el: wrapperEl, 180 | level: -1, 181 | // root的data需要特殊处理,通过-1取到 182 | data, 183 | index: 0, 184 | parent: null, 185 | childrenList: [], 186 | type: "root", 187 | isShow: true 188 | }; 189 | mapping.set(wrapperEl, latestDomInfo); 190 | 191 | // 获取tr列表,同时规避占位用的dom 192 | const trList = Array.from(wrapperEl.querySelectorAll("tr")) 193 | .filter(tr => { 194 | return !tr.classList.contains(PLACEHOLDER_CSS) 195 | }) 196 | 197 | trList.forEach((tr, index) => { 198 | try { 199 | const { className, style } = tr; 200 | const isShow = style.display === 'none' ? false : true 201 | 202 | /** @type {DomInfo} */ 203 | const domInfo = { 204 | elIndex: index, 205 | el: tr, 206 | level: 0, 207 | data, 208 | type: 'common', 209 | index: 0, 210 | parent: null, 211 | childrenList: [], 212 | isShow 213 | }; 214 | 215 | /** 216 | * expanded的容器行 217 | * 相当于其父容器的代理 218 | * 自动和最近那个操作的行绑定,因为没有明确的类名称,所以需要特殊处理 219 | */ 220 | if (!className) { 221 | if (latestDomInfo) { 222 | Object.assign(domInfo, { 223 | ...latestDomInfo, 224 | childrenList: [], 225 | el: tr, 226 | elIndex: index, 227 | type: "proxy", 228 | }); 229 | latestDomInfo.childrenList.push(domInfo); 230 | } 231 | mapping.set(tr, domInfo); 232 | return; 233 | } 234 | 235 | // 创建dom对应的信息 236 | const level = getLevelFromClassName(tr.className); 237 | domInfo.level = level; 238 | 239 | /** 240 | * 这里需要两个步骤,如果相差一级的话,当作是parent, 241 | * 如果超过一级的话,需要回朔查找同级别的对象,以其为基准继续判定 242 | * 没有tree的时候默认都为同级 243 | */ 244 | const levelGap = level - latestDomInfo.level 245 | switch (levelGap) { 246 | // 同级,继承 247 | case 0: { 248 | domInfo.index = latestDomInfo.index + 1; 249 | domInfo.parent = latestDomInfo.parent; 250 | domInfo.data = latestDomInfo.data; 251 | 252 | if (domInfo.parent) { 253 | domInfo.parent.childrenList.push(domInfo); 254 | } 255 | 256 | break; 257 | } 258 | // 之前的那个tr的下级 259 | case 1: { 260 | domInfo.parent = latestDomInfo; 261 | 262 | const childrenData = 263 | latestDomInfo.type === "root" 264 | ? data 265 | : latestDomInfo.data[latestDomInfo.index][children]; 266 | domInfo.data = childrenData; 267 | domInfo.parent.childrenList.push(domInfo); 268 | break; 269 | } 270 | // 正常情况,朔源最新的一个同级的 271 | default: { 272 | const sameLevelDomInfo = getSameLevelParentDomInfo( 273 | latestDomInfo, 274 | level 275 | ); 276 | if (!sameLevelDomInfo) { 277 | console.error(tr, latestDomInfo); 278 | throw new Error("找不到其同级dom"); 279 | } 280 | domInfo.index = sameLevelDomInfo.index + 1; 281 | domInfo.parent = sameLevelDomInfo.parent; 282 | domInfo.data = sameLevelDomInfo.data; 283 | if (domInfo.parent) { 284 | domInfo.parent.childrenList.push(domInfo); 285 | } 286 | break; 287 | } 288 | } 289 | mapping.set(tr, domInfo); 290 | latestDomInfo = domInfo; 291 | } catch (e) { 292 | console.error({ 293 | tr, 294 | latestDomInfo, 295 | }); 296 | console.error(e); 297 | } 298 | }); 299 | 300 | observer && observer.start(); 301 | return mapping; 302 | } 303 | 304 | export class MappingOberver { 305 | /** 306 | * @param {Vue} elTableInstance 307 | * @param {string} wrapper 308 | */ 309 | constructor(elTableInstance, wrapper = ".el-table__body-wrapper tbody") { 310 | this.elTableInstance = elTableInstance; 311 | this.mapping = new Map(); 312 | this.wrapper = wrapper; 313 | this.observer = new MutationObserver(() => { 314 | createOrUpdateDomMapping( 315 | this.elTableInstance, 316 | this.mapping, 317 | wrapper, 318 | this 319 | ); 320 | }); 321 | } 322 | rebuild() { 323 | createOrUpdateDomMapping( 324 | this.elTableInstance, 325 | this.mapping, 326 | this.wrapper, 327 | this 328 | ); 329 | } 330 | start() { 331 | this.observer.observe( 332 | this.elTableInstance.$el.querySelector(this.wrapper), 333 | { 334 | childList: true, 335 | subtree: true, 336 | attributes: true, 337 | attributeFilter: ['style'] 338 | } 339 | ); 340 | } 341 | stop() { 342 | this.observer.disconnect(); 343 | } 344 | } 345 | 346 | /** 347 | * 将某个元素从某个列表插入到另一个对应位置 348 | * @param {number} oldIndex 349 | * @param {any[]} fromList 350 | * @param {nmber} newIndex 351 | * @param {any[]} toList 352 | * @param {import('@types/sortablejs').PullResult} pullMode 353 | */ 354 | export function exchange(oldIndex, fromList, newIndex, toList, pullMode) { 355 | // 核心交换 356 | const target = fromList[oldIndex]; 357 | // move的情况 358 | if (pullMode !== "clone") { 359 | fromList.splice(oldIndex, 1); 360 | } 361 | toList.splice(newIndex, 0, target); 362 | } 363 | 364 | /** 365 | * 通知收到影响的表格 366 | * @param {Element} from 367 | * @param {Element} to 368 | * @param {Map} context 369 | * @param {(tableInstance: Vue) => any} handler 370 | */ 371 | export function updateElTableInstance(from, to, context, handler) { 372 | const affected = from === to ? [from] : [from, to]; 373 | affected.forEach((table) => { 374 | if (context.has(table)) { 375 | const tableInstance = context.get(table); 376 | handler(tableInstance); 377 | } 378 | }); 379 | } 380 | 381 | export default { 382 | checkIsTreeTable, 383 | // fixDomInfoByDirection, 384 | addTreePlaceholderRows, 385 | getOnMove, 386 | exchange, 387 | updateElTableInstance 388 | }; 389 | -------------------------------------------------------------------------------- /types/DomInfo.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-entry-file 2 | export interface DomInfo { 3 | el: Element 4 | elIndex: number 5 | level: number 6 | data: any[] 7 | index: number 8 | parent: DomInfo | null 9 | childrenList: DomInfo[] 10 | isShow: boolean 11 | type: 'root' | 'common' | 'proxy' | 'placeholder' 12 | } 13 | 14 | export type DomMapping = Map -------------------------------------------------------------------------------- /vetur/attributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": { 3 | "description": "// or { name: \"...\", pull: [true, false, 'clone', array], put: [true, false, array] }" 4 | } 5 | } -------------------------------------------------------------------------------- /vetur/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "el-table-draggable": { 3 | "attributes": [ 4 | "group", 5 | "sort", 6 | "delay", 7 | "touchStartThreshold", 8 | "disabled", 9 | "store", 10 | "animation", 11 | "easing", 12 | "handle", 13 | "filter", 14 | "preventOnFilter", 15 | "draggable", 16 | "ghostClass", 17 | "chosenClass", 18 | "dragClass", 19 | "dataIdAttr", 20 | "swapThreshold", 21 | "invertSwap", 22 | "invertedSwapThreshold", 23 | "direction", 24 | "forceFallback", 25 | "fallbackClass", 26 | "fallbackOnBody", 27 | "fallbackTolerance", 28 | "scroll", 29 | "scrollFn", 30 | "scrollSensitivity", 31 | "scrollSpeed", 32 | "bubbleScroll", 33 | "dragoverBubble", 34 | "removeCloneOnHide", 35 | "emptyInsertThreshold" 36 | ], 37 | "subtags": [ 38 | "el-table" 39 | ], 40 | "description": "让el-table可拖动" 41 | } 42 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: "/el-table-draggable/", 3 | configureWebpack: { 4 | output: { 5 | libraryExport: 'default' 6 | } 7 | }, 8 | css: { 9 | extract: false 10 | } 11 | } --------------------------------------------------------------------------------