├── .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 |
62 |
63 |
64 |
65 |
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 |
55 |
56 |
57 |
58 |
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 |
2 |
3 |
Demo
4 |
5 |
12 |
13 |
{{ name }}
14 |
15 |
20 | 查看源文件
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
163 |
197 |
--------------------------------------------------------------------------------
/src/components/ListViewer.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ displayed }}
3 |
4 |
5 |
20 |
--------------------------------------------------------------------------------
/src/examples/Base.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/examples/Column.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
45 |
--------------------------------------------------------------------------------
/src/examples/ColumnWithHandler.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
10 |
11 |
12 | {{ column.label }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
48 |
--------------------------------------------------------------------------------
/src/examples/DataTree.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
52 |
--------------------------------------------------------------------------------
/src/examples/DataTreeSampleParent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
75 |
--------------------------------------------------------------------------------
/src/examples/Handle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/examples/MultiTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/examples/MultiTableClone.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/examples/MultiTableGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/examples/MultiTableNestint.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/examples/SimpleTableNestint.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
12 |
16 |
17 | helloworld {{ row.index }}
18 |
37 |
38 |
39 |
45 |
46 |
47 |
48 |
52 |
53 |
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 | }
--------------------------------------------------------------------------------