├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .travis.yml
├── LICENSE
├── build
├── webpack.base.config.js
├── webpack.demo.config.js
├── webpack.dev.config.js
├── webpack.dist.config.js
└── webpack.test.config.js
├── examples
├── app.vue
├── demo.html
├── dist
│ ├── 10.chunk.js
│ ├── 11.chunk.js
│ ├── 12.chunk.js
│ ├── 13.chunk.js
│ ├── 14.chunk.js
│ ├── 15.chunk.js
│ ├── 2.chunk.js
│ ├── 3.chunk.js
│ ├── 4.chunk.js
│ ├── 5.chunk.js
│ ├── 6.chunk.js
│ ├── 7.chunk.js
│ ├── 8.chunk.js
│ ├── 9.chunk.js
│ ├── index.html
│ ├── main.js
│ └── vendors.js
├── features
│ ├── 123.png
│ ├── addColor.vue
│ ├── autoCalWidth.vue
│ ├── expand.vue
│ ├── expandRow.vue
│ ├── fixedHeader.vue
│ ├── fixedLeft.vue
│ ├── fixedLeft1.vue
│ ├── fixedLeft2.vue
│ ├── footer.vue
│ ├── index.vue
│ ├── loading.vue
│ ├── render.vue
│ ├── resizable.vue
│ ├── scopedSlot.vue
│ ├── selectable.vue
│ ├── sortable.vue
│ ├── span.vue
│ └── style.vue
├── index.html
├── main.js
└── routes.js
├── index.js
├── package-lock.json
├── package.json
├── readme.md
├── src
├── Spinner.vue
├── css
│ └── main.less
├── data.js
├── expand.js
├── mixin.js
├── slot.js
├── table.vue
├── tableBody.vue
├── tableFoot.vue
├── tableHead.vue
├── tableLoadingBar.vue
├── tableScrollBar.vue
├── tableSum.vue
├── tableTd.vue
└── tableTr.vue
├── test
├── index.d.ts
├── tsconfig.json
├── tslint.json
└── unit
│ ├── index.js
│ ├── karma.conf.js
│ ├── specs
│ ├── expand.spec.ts
│ ├── fixed.spec.ts
│ ├── index.spec.ts
│ ├── initRowNumber.spec.ts
│ ├── loading.spec.ts
│ ├── render.spec.ts
│ ├── resizable.spec.ts
│ ├── scopedSlot.spec.ts
│ ├── scrollBar.spec.ts
│ ├── selectable.spec.ts
│ ├── sortable.spec.ts
│ └── theme.spec.ts
│ ├── tool.ts
│ └── util.ts
├── types
└── index.d.ts
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": "commonjs",
7 | }
8 | ]
9 | ],
10 | "env": {
11 | "test": {
12 | "plugins": [
13 | "istanbul"
14 | ]
15 | }
16 | },
17 | "plugins": [
18 | "@babel/plugin-transform-runtime"
19 | ]
20 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*.{js,css}]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 4
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: 'babel-eslint',
3 | extends: ['airbnb'],
4 | rules: {
5 | 'arrow-body-style': 0,
6 | strict: 0,
7 | 'no-console': 0,
8 | 'func-names': 0,
9 | 'space-before-function-paren': 0,
10 | 'no-param-reassign': 0,
11 | 'import/no-dynamic-require': 0,
12 | 'global-require': 0,
13 | 'consistent-return': 0,
14 | "indent": [2, 4]
15 | },
16 | };
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Vue Version [e.g. 2.6.5]
30 | - FlexTable Version [e.g. 0.0.5]
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ## Checklist
10 |
11 |
12 |
13 | * [ ] `npm test` passes
14 | * [ ] tests are included
15 | * [ ] documentation is changed or added
16 | * [ ] commit message follows commit guidelines
17 |
18 | ## Affected feature(s)
19 |
20 |
21 | ## Description of change
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.log.*
3 | .idea/
4 | .DS_Store
5 | Thumbs.db
6 | .project
7 | .*proj
8 | .svn/
9 | .build
10 | node_modules
11 | .cache
12 | example/dist
13 | **/coverage
14 | dist
15 | .history
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | sudo: false
4 |
5 | node_js:
6 | - 11
7 |
8 | before_install:
9 | - |
10 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/'
11 | then
12 | echo "Only docs were updated, stopping build process."
13 | exit
14 | fi
15 | npm install
16 | npm install -g codecov
17 |
18 | script:
19 | - |
20 | if [ "$TEST_TYPE" = test ]; then
21 | npm test
22 | else
23 | npm run $TEST_TYPE
24 | fi
25 | env:
26 | matrix:
27 | - TEST_TYPE=lint
28 | - TEST_TYPE=coverage
29 |
30 | after_success:
31 | - codecov
32 |
33 | branches:
34 | only:
35 | - master
36 |
37 | notifications:
38 | webhooks: https://oapi.dingtalk.com/robot/send?access_token=fe13eaac0e256bce410fd755deaa489f89cc3ce7969a0db9ebfc9354e2296b90
39 | email:
40 | - lb.robin1991@gmail.com
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 tm-fe
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.
22 |
--------------------------------------------------------------------------------
/build/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const pkg = require('../package.json');
4 | const { VueLoaderPlugin } = require('vue-loader');
5 |
6 | function resolve(dir) {
7 | return path.join(__dirname, '..', dir);
8 | }
9 | const sourceMap = false; // css sourceMap
10 | let jsSourceMap = true;
11 | if (process.env.NODE_ENV === 'production') {
12 | jsSourceMap = false;
13 | }
14 | module.exports = {
15 | entry: '../index.js',
16 | module: {
17 | // https://doc.webpack-china.org/guides/migrating/#module-loaders-module-rules
18 | rules: [
19 | {
20 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
21 | test: /\.vue$/,
22 | loader: 'vue-loader',
23 | options: {
24 | loaders: {
25 | css: [
26 | 'vue-style-loader',
27 | {
28 | loader: 'css-loader',
29 | options: {
30 | sourceMap,
31 | },
32 | },
33 | ],
34 | less: [
35 | 'vue-style-loader',
36 | {
37 | loader: 'css-loader',
38 | options: {
39 | sourceMap,
40 | },
41 | },
42 | {
43 | loader: 'less-loader',
44 | options: {
45 | sourceMap,
46 | },
47 | },
48 | ],
49 | scss: [
50 | 'vue-style-loader',
51 | {
52 | loader: 'css-loader',
53 | options: {
54 | sourceMap,
55 | },
56 | },
57 | {
58 | loader: 'scss-loader',
59 | options: {
60 | sourceMap,
61 | },
62 | },
63 | ],
64 | },
65 | postLoaders: {
66 | html: 'babel-loader?sourceMap',
67 | },
68 | sourceMap: jsSourceMap,
69 | },
70 | },
71 | {
72 | test: /\.js$/,
73 | loader: 'babel-loader',
74 | options: {
75 | sourceMap: jsSourceMap,
76 | },
77 | exclude: /node_modules/,
78 | },
79 | {
80 | test: /\.css$/,
81 | loaders: [
82 | {
83 | loader: 'style-loader',
84 | options: {
85 | sourceMap,
86 | },
87 | },
88 | {
89 | loader: 'css-loader',
90 | options: {
91 | sourceMap,
92 | },
93 | },
94 | ],
95 | },
96 | {
97 | test: /\.less$/,
98 | loaders: [
99 | {
100 | loader: 'style-loader',
101 | options: {
102 | sourceMap,
103 | },
104 | },
105 | {
106 | loader: 'css-loader',
107 | options: {
108 | sourceMap,
109 | },
110 | },
111 | {
112 | loader: 'less-loader',
113 | options: {
114 | sourceMap,
115 | },
116 | },
117 | ],
118 | },
119 | {
120 | test: /\.scss$/,
121 | loaders: [
122 | {
123 | loader: 'style-loader',
124 | options: {
125 | sourceMap,
126 | },
127 | },
128 | {
129 | loader: 'css-loader',
130 | options: {
131 | sourceMap,
132 | },
133 | },
134 | {
135 | loader: 'sass-loader',
136 | options: {
137 | sourceMap,
138 | },
139 | },
140 | ],
141 | },
142 | {
143 | test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
144 | loader: 'url-loader?limit=8192',
145 | },
146 | {
147 | test: /\.(html|tpl)$/,
148 | loader: 'html-loader',
149 | },
150 | {
151 | test: /\.tsx?$/,
152 | use: 'ts-loader',
153 | },
154 | ],
155 | },
156 | resolve: {
157 | extensions: ['.js', '.vue'],
158 | alias: {
159 | vue: 'vue/dist/vue.esm.js',
160 | '@': resolve('src'),
161 | },
162 | },
163 | plugins: [
164 | new webpack.optimize.ModuleConcatenationPlugin(),
165 | new webpack.DefinePlugin({
166 | 'process.env.VERSION': `'${pkg.version}'`,
167 | }),
168 | new VueLoaderPlugin(),
169 | ],
170 | };
171 |
--------------------------------------------------------------------------------
/build/webpack.demo.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const merge = require('webpack-merge');
5 | const webpackBaseConfig = require('./webpack.base.config.js');
6 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
7 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
8 |
9 |
10 | module.exports = merge(webpackBaseConfig, {
11 | // devtool: 'eval-source-map',
12 | entry: {
13 | main: './examples/main',
14 | vendors: ['vue', 'vue-router']
15 | },
16 | output: {
17 | path: path.join(__dirname, '../examples/dist'),
18 | publicPath: '',
19 | filename: '[name].js',
20 | chunkFilename: '[name].chunk.js'
21 | },
22 | resolve: {
23 | alias: {
24 | vue: 'vue/dist/vue.esm.js',
25 | }
26 | },
27 | plugins: [
28 | // new webpack.optimize.CommonsChunkPlugin({ name: 'vendors', filename: 'vendor.bundle.js' }),
29 | new HtmlWebpackPlugin({
30 | inject: true,
31 | filename: path.join(__dirname, '../examples/dist/index.html'),
32 | template: path.join(__dirname, '../examples/index.html')
33 | }),
34 | new FriendlyErrorsPlugin(),
35 | new webpack.DefinePlugin({
36 | 'process.env': {
37 | NODE_ENV: '"production"'
38 | }
39 | }),
40 | new UglifyJsPlugin({
41 | parallel: true,
42 | sourceMap: false,
43 | })
44 | ],
45 | mode: 'production',
46 | });
47 |
--------------------------------------------------------------------------------
/build/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const merge = require('webpack-merge');
5 | const webpackBaseConfig = require('./webpack.base.config.js');
6 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
7 |
8 |
9 | module.exports = merge(webpackBaseConfig, {
10 | devtool: 'eval-source-map',
11 | entry: {
12 | main: './examples/main',
13 | vendors: ['vue', 'vue-router']
14 | },
15 | output: {
16 | path: path.join(__dirname, '../examples/dist'),
17 | publicPath: '',
18 | filename: '[name].js',
19 | chunkFilename: '[name].chunk.js'
20 | },
21 | resolve: {
22 | alias: {
23 | vue: 'vue/dist/vue.esm.js'
24 | }
25 | },
26 | optimization: {
27 | splitChunks: {
28 | chunks: 'async',
29 | minSize: 30000,
30 | maxSize: 0,
31 | minChunks: 1,
32 | automaticNameDelimiter: '~',
33 | name: true,
34 | }
35 | },
36 | plugins: [
37 | // new webpack.optimize.CommonsChunkPlugin({ name: 'vendors', filename: 'vendor.bundle.js' }),
38 | new HtmlWebpackPlugin({
39 | inject: true,
40 | filename: path.join(__dirname, '../examples/dist/index.html'),
41 | template: path.join(__dirname, '../examples/index.html')
42 | }),
43 | new FriendlyErrorsPlugin()
44 | ]
45 | });
--------------------------------------------------------------------------------
/build/webpack.dist.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const merge = require('webpack-merge');
4 | const webpackBaseConfig = require('./webpack.base.config.js');
5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
6 |
7 | process.env.NODE_ENV = 'production';
8 |
9 | module.exports = merge(webpackBaseConfig, {
10 | // devtool: 'source-map',
11 | entry: {
12 | main: './index.js',
13 | },
14 | output: {
15 | path: path.resolve(__dirname, '../dist'),
16 | publicPath: '/dist/',
17 | filename: 'index.js',
18 | library: 'FlexTable',
19 | libraryTarget: 'umd',
20 | umdNamedDefine: true,
21 | },
22 | externals: {
23 | vue: {
24 | root: 'Vue',
25 | commonjs: 'vue',
26 | commonjs2: 'vue',
27 | amd: 'vue',
28 | }
29 | },
30 | plugins: [
31 | new webpack.DefinePlugin({
32 | 'process.env': {
33 | NODE_ENV: '"production"',
34 | },
35 | }),
36 | new UglifyJsPlugin({
37 | parallel: true,
38 | sourceMap: false,
39 | }),
40 | ],
41 | mode: 'production',
42 | });
43 |
--------------------------------------------------------------------------------
/build/webpack.test.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const merge = require('webpack-merge');
3 | const webpackBaseConfig = require('./webpack.base.config.js');
4 |
5 | function resolve(dir) {
6 | return path.join(__dirname, '..', dir);
7 | }
8 |
9 | const conifg = merge(webpackBaseConfig, {
10 | devtool: 'eval-source-map',
11 | mode: 'development',
12 | resolve: {
13 | extensions: ['.js', '.vue', '.ts'],
14 | alias: {
15 | vue: 'vue/dist/vue.esm.js',
16 | '@': resolve('test/unit'),
17 | },
18 | },
19 | });
20 |
21 | delete conifg.entry;
22 | module.exports = conifg;
23 |
--------------------------------------------------------------------------------
/examples/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
35 |
113 |
--------------------------------------------------------------------------------
/examples/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | iview example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/examples/dist/10.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[10],{132:function(e,t,n){"use strict";function r(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("h3",[e._v("render function")]),e._v(" "),e._m(0),e._v(" "),n("flex-table",{attrs:{loading:e.loading,columns:e.columns,data:e.list,sum:e.sum,height:e.height}})],1)}var a=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("p",[e._v("render & renderHeader: "),n("a",{attrs:{href:"https://github.com/tm-fe/FlexTable/blob/master/examples/features/render.vue"}},[e._v("source code")])])}];n.d(t,"a",function(){return r}),n.d(t,"b",function(){return a})},78:function(e,t,n){"use strict";n.r(t);var r=n(132),a=n(98);for(var u in a)"default"!==u&&function(e){n.d(t,e,function(){return a[e]})}(u);var i=n(0),o=Object(i.a)(a.default,r.a,r.b,!1,null,null,null);t.default=o.exports},98:function(e,t,n){"use strict";n.r(t);var r=n(99),a=n.n(r);for(var u in r)"default"!==u&&function(e){n.d(t,e,function(){return r[e]})}(u);t.default=a.a},99:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;for(var r=[],a=0;a<20;a++)r.push({name:"John Brown",age:18,address:"New York No. 1 Lake Park",date:"2016-10-03"});var u={data:function(){return{columns:[{title:"Name",key:"name",renderHeader:function(e,t){return e("span","Custom Title : "+t.column.title)}},{title:"Age",key:"age",render:function(e,t){return e("span","age: "+t.row.age)}},{title:"Address",key:"address",width:300},{title:"Date",key:"date"}],loading:!1,list:r,sum:{name:"Jim Green",age:24,address:"London",date:"2016-10-01"},height:250}}};t.default=u}}]);
--------------------------------------------------------------------------------
/examples/dist/11.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[11],{102:function(e,t,n){"use strict";n.r(t);var o=n(103),a=n.n(o);for(var r in o)"default"!==r&&function(e){n.d(t,e,function(){return o[e]})}(r);t.default=a.a},103:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;for(var o=[],a=0;a<20;a++)o.push({name:"John Brown",age:18,address:"New York No. 1 Lake Park",date:"2016-10-03"});var r={data:function(){return{columns:[{title:"Name",key:"name",width:100,fixed:"left",sortable:!0,minWidth:100,maxWidth:200},{title:"Age",key:"age",sortable:!0,render:function(e,t){return e("span","age: "+t.row.age)}},{title:"Address",key:"address",width:300},{title:"Date",key:"date",sortable:!0}],loading:!1,list:o,sum:{name:"Jim Green",age:24,address:"London",date:"2016-10-01"},height:250}},methods:{onSortChange:function(e){console.log(e)},onResizeWidth:function(e,t,n,o){console.log("newWidth--".concat(e)),console.log("oldWidth--".concat(t)),console.log("column--".concat(JSON.stringify(n))),console.log(o)}}};t.default=r},134:function(e,t,n){"use strict";function o(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("h3",[e._v("调整宽度")]),e._v(" "),e._m(0),e._v(" "),n("flex-table",{attrs:{resizable:"",loading:e.loading,columns:e.columns,data:e.list,sum:e.sum,height:e.height,minWidth:40,maxWidth:300},on:{"on-col-width-resize":e.onResizeWidth,"on-sort-change":e.onSortChange}})],1)}var a=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("p",[e._v("拖动调整宽度 "),n("a",{attrs:{href:"https://github.com/tm-fe/FlexTable/blob/master/examples/features/resizable.vue"}},[e._v("source code")])])}];n.d(t,"a",function(){return o}),n.d(t,"b",function(){return a})},80:function(e,t,n){"use strict";n.r(t);var o=n(134),a=n(102);for(var r in a)"default"!==r&&function(e){n.d(t,e,function(){return a[e]})}(r);var i=n(0),s=Object(i.a)(a.default,o.a,o.b,!1,null,null,null);t.default=s.exports}}]);
--------------------------------------------------------------------------------
/examples/dist/12.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[12],{110:function(e,t,n){"use strict";n.r(t);var o=n(111),r=n.n(o);for(var a in o)"default"!==a&&function(e){n.d(t,e,function(){return o[e]})}(a);t.default=r.a},111:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;for(var o=[],r=0;r<10;r++)o.push({name:"John Brown",age:18,address:"New York No. 1 Lake Park",date:"2016-10-03"});var a={data:function(){return{columns:[{title:"Name",key:"name"},{title:"Age",key:"age",render:function(e,t){return e("span","age: "+t.row.age)}},{title:"Address",key:"address"},{title:"Date",key:"date"},{title:"operation",key:"operation",type:"slot"}],loading:!1,list:o,sum:{name:"Jim Green",age:24,address:"London",date:"2016-10-01"}}},mounted:function(){},methods:{show:function(e){alert("show "+e)},remove:function(e){alert("remove "+e)}}};t.default=a},137:function(e,t,n){"use strict";function o(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("h3",[e._v("scoped-slot")]),e._v(" "),e._m(0),e._v(" "),n("flex-table",{attrs:{resizable:"",loading:e.loading,columns:e.columns,data:e.list,sum:e.sum},scopedSlots:e._u([{key:"operation",fn:function(t){t.row;var o=t.index;return[n("button",{staticStyle:{"margin-right":"5px"},on:{click:function(t){return e.show(o)}}},[e._v("View")]),e._v(" "),n("button",{on:{click:function(t){return e.remove(o)}}},[e._v("Delete")])]}}])})],1)}var r=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("p",[e._v("scoped-slot "),n("a",{attrs:{href:"https://github.com/tm-fe/FlexTable/blob/master/examples/features/scopedSlot.vue"}},[e._v("source code")])])}];n.d(t,"a",function(){return o}),n.d(t,"b",function(){return r})},83:function(e,t,n){"use strict";n.r(t);var o=n(137),r=n(110);for(var a in r)"default"!==a&&function(e){n.d(t,e,function(){return r[e]})}(a);var u=n(0),i=Object(u.a)(r.default,o.a,o.b,!1,null,null,null);t.default=i.exports}}]);
--------------------------------------------------------------------------------
/examples/dist/13.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[13],{104:function(e,n,t){"use strict";t.r(n);var a=t(105),o=t.n(a);for(var l in a)"default"!==l&&function(e){t.d(n,e,function(){return a[e]})}(l);n.default=o.a},105:function(e,n,t){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.default=void 0;for(var a=[],o=0;o<80;o++)a.push({name:"John Brown",age:18,address:"New York No. 1 Lake Park",real_address:"New York No. 1 Lake Park",date:"2016-10-03"});var l={data:function(){return{columns:[{type:"selection",width:20,align:"center",fixed:"left"},{title:"Name",key:"name",width:150},{title:"Age",key:"age",width:150,render:function(e,n){return e("span","age: "+n.row.age)}},{title:"Address",key:"address",width:250},{title:"Real Address",key:"real_address",width:250},{title:"Date",key:"date",width:250}],loading:!1,list:a,sum:{name:"Jim Green",age:24,address:"London",date:"2016-10-01"}}},mounted:function(){},methods:{onSelectionChange:function(e,n){console.log("onSelectionChange",e,n)},onSelectionCancel:function(e){console.log("onSelectionCancel",e)},onAllCancel:function(e){console.log("onAllCancel",e)}}};n.default=l},135:function(e,n,t){"use strict";function a(){var e=this,n=e.$createElement,t=e._self._c||n;return t("div",[t("h3",[e._v("多选")]),e._v(" "),e._m(0),e._v(" "),t("flex-table",{attrs:{loading:e.loading,columns:e.columns,data:e.list,sum:e.sum,"fixed-head":!0,"async-render":10},on:{"on-selection-change":e.onSelectionChange,"on-selection-cancel":e.onSelectionCancel,"on-all-cancel":e.onAllCancel}})],1)}var o=[function(){var e=this,n=e.$createElement,t=e._self._c||n;return t("p",[e._v("选择行 "),t("a",{attrs:{href:"https://github.com/tm-fe/FlexTable/blob/master/examples/features/selectable.vue"}},[e._v("source code")])])}];t.d(n,"a",function(){return a}),t.d(n,"b",function(){return o})},81:function(e,n,t){"use strict";t.r(n);var a=t(135),o=t(104);for(var l in o)"default"!==l&&function(e){t.d(n,e,function(){return o[e]})}(l);var r=t(0),c=Object(r.a)(o.default,a.a,a.b,!1,null,null,null);n.default=c.exports}}]);
--------------------------------------------------------------------------------
/examples/dist/14.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[14],{131:function(e,t,n){"use strict";function a(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("h3",[e._v("排序")]),e._v(" "),e._m(0),e._v(" "),n("flex-table",{attrs:{loading:e.loading,columns:e.columns,data:e.list},on:{"on-sort-change":e.onSortChange}})],1)}var r=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("p",[n("a",{attrs:{href:"https://github.com/tm-fe/FlexTable/blob/master/examples/features/sortable.vue"}},[e._v("source code")])])}];n.d(t,"a",function(){return a}),n.d(t,"b",function(){return r})},77:function(e,t,n){"use strict";n.r(t);var a=n(131),r=n(96);for(var o in r)"default"!==o&&function(e){n.d(t,e,function(){return r[e]})}(o);var u=n(0),l=Object(u.a)(r.default,a.a,a.b,!1,null,null,null);t.default=l.exports},96:function(e,t,n){"use strict";n.r(t);var a=n(97),r=n.n(a);for(var o in a)"default"!==o&&function(e){n.d(t,e,function(){return a[e]})}(o);t.default=r.a},97:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;for(var a=[],r=0;r<10;r++)a.push({name:"John Brown",age:18,address:"New York No. 1 Lake Park",date:"2016-10-03"});var o={data:function(){return{columns:[{title:"Name",key:"name",width:100,sortable:!0},{title:"Age",key:"age",sortable:!0},{title:"Address",key:"address",width:300},{title:"Date",key:"date",sortable:!0}],loading:!1,list:a}},methods:{onSortChange:function(e){console.log(e)}}};t.default=o}}]);
--------------------------------------------------------------------------------
/examples/dist/15.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[15],{118:function(e,t,n){"use strict";n.r(t);var o=n(119),r=n.n(o);for(var a in o)"default"!==a&&function(e){n.d(t,e,function(){return o[e]})}(a);t.default=r.a},119:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;for(var o=[],r=0;r<5;r++)o.push({name:"John Brown",age:18,sex:"男",school:"high school",color:"red",address:"New York No. 1 Lake Park",date:"2016-10-03"});var a={data:function(){return{columns:[{title:"Name",key:"name",width:240},{title:"Age",key:"age",width:140,render:function(e,t){return e("span","age: "+t.row.age)}},{title:"Address",key:"address",width:240},{title:"Sex",key:"sex",width:140},{title:"School",key:"school",width:240},{title:"Color",key:"color",width:140},{title:"Date",key:"date",width:240}],loading:!1,list:o,sum:{name:"Jim Green",age:24,address:"London",date:"2016-10-01"}}},mounted:function(){},methods:{onTableScroll:function(e){console.log(e.target.scrollLeft)},spanMethod:function(e){if("age"===e.column.key)return e.rowIndex?{rowspan:0,colspan:1}:{rowspan:5,colspan:1};if("address"===e.column.key){if(1===e.rowIndex)return{rowspan:2,colspan:1};if(2===e.rowIndex)return{rowspan:0,colspan:1}}return{rowspan:1,colspan:1}}}};t.default=a},140:function(e,t,n){"use strict";function o(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("h3",[e._v("合并行")]),e._v(" "),e._m(0),e._v(" "),n("flex-table",{attrs:{resizable:"",loading:e.loading,columns:e.columns,data:e.list,sum:e.sum,minWidth:80,maxWidth:600,"span-method":e.spanMethod},on:{"on-scroll-x":e.onTableScroll}})],1)}var r=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("p",[e._v("span "),n("a",{attrs:{href:"https://github.com/tm-fe/FlexTable/blob/master/examples/features/span.vue"}},[e._v("source code")])])}];n.d(t,"a",function(){return o}),n.d(t,"b",function(){return r})},86:function(e,t,n){"use strict";n.r(t);var o=n(140),r=n(118);for(var a in r)"default"!==a&&function(e){n.d(t,e,function(){return r[e]})}(a);var s=n(0),l=Object(s.a)(r.default,o.a,o.b,!1,null,null,null);t.default=l.exports}}]);
--------------------------------------------------------------------------------
/examples/dist/2.chunk.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[2],{106:function(e,t,n){"use strict";n.r(t);var a=n(107),r=n.n(a);for(var s in a)"default"!==s&&function(e){n.d(t,e,function(){return a[e]})}(s);t.default=r.a},107:function(e,t,n){"use strict";var a=n(1);function r(){for(var e=0
2 |
3 |
4 |
5 |
6 | flextable test page
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/features/123.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tm-fe/FlexTable/f0711963d9571f1422470bb0ed42e254a86fd1b0/examples/features/123.png
--------------------------------------------------------------------------------
/examples/features/addColor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
78 |
79 |
96 |
--------------------------------------------------------------------------------
/examples/features/autoCalWidth.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
自动计算宽度-false
4 |
自动计算宽度-false source code
5 |
6 |
17 |
18 |
19 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/features/expand.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
展开功能
4 |
表格行展开 source code
5 |
刷新数据
6 |
12 |
13 |
expand scoped slot 用法
14 |
22 |
23 |
24 |
第 {{ index+1 }} 行
25 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/examples/features/expandRow.vue:
--------------------------------------------------------------------------------
1 |
2 | This is an expand example
3 |
4 |
--------------------------------------------------------------------------------
/examples/features/fixedHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
固定表头
4 |
固定表头 source code
5 |
6 |
13 |
14 |
15 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/features/fixedLeft.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
固定列
4 |
5 | 左右固定列
6 | source code
10 |
11 |
show
12 |
24 |
25 |
26 |
![]()
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/features/fixedLeft1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
固定列
4 |
5 | 左右固定列
6 | source code
10 |
11 |
切换
12 |
21 |
22 |
23 |
![]()
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/features/fixedLeft2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
固定列
4 |
5 | 左右固定列
6 | source code
10 |
11 |
切换
12 |
24 |
25 |
26 |
![]()
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/features/footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | {{row.age}}
10 |
11 |
12 | 汇总
13 | {{row[key]}}
14 |
15 |
16 |
17 |
72 |
--------------------------------------------------------------------------------
/examples/features/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
基础用法
4 |
5 | 表格的简单用法
6 | source code
10 |
11 |
12 |
27 |
28 | slotSum
29 |
30 | {{ row.name }}
31 |
32 |
33 |
34 |
35 |
36 |
189 |
--------------------------------------------------------------------------------
/examples/features/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/features/render.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
render function
4 |
render & renderHeader: source code
5 |
6 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/examples/features/resizable.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/features/scopedSlot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
scoped-slot
4 |
scoped-slot source code
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/features/selectable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
多选
4 |
5 | 选择行
6 | source code
10 |
11 |
12 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/features/sortable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
排序
4 |
source code
5 |
6 |
12 |
13 | 123
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/features/span.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/features/style.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
风格样式
4 |
风格样式展示 source code
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
26 |
27 |
28 |
29 |
88 |
97 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flextable test page
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | // import 'babel-polyfill';
2 | import Vue from 'vue';
3 | import VueRouter from 'vue-router';
4 | import routes from './routes';
5 | import App from './app.vue';
6 | import FlexTable from '../index.js';
7 |
8 | Vue.use(VueRouter);
9 | Vue.use(FlexTable);
10 |
11 | // 开启debug模式
12 | Vue.config.debug = true;
13 |
14 | // 路由配置
15 | const router = new VueRouter({
16 | esModule: false,
17 | mode: 'hash',
18 | routes,
19 | });
20 |
21 | const app = new Vue({
22 | router,
23 | render: h => h(App),
24 | }).$mount('#app');
25 |
--------------------------------------------------------------------------------
/examples/routes.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: '/',
4 | name: 'index',
5 | component: resolve => require(['./features/index.vue'], resolve),
6 | meta: {
7 | title: '普通用法',
8 | },
9 | },
10 | {
11 | path: '/autoCalWidth',
12 | name: 'autoCalWidth',
13 | component: resolve => require(['./features/autoCalWidth.vue'], resolve),
14 | meta: {
15 | title: '自动计算宽度',
16 | },
17 | },
18 | {
19 | path: '/fixedHeader',
20 | name: 'fixedHeader',
21 | component: resolve => require(['./features/fixedHeader.vue'], resolve),
22 | meta: {
23 | title: '固定头部',
24 | },
25 | },
26 | {
27 | path: '/footer',
28 | name: 'footer',
29 | component: resolve => require(['./features/footer.vue'], resolve),
30 | meta: {
31 | title: '底部汇总',
32 | },
33 | },
34 | {
35 | path: '/loading',
36 | name: 'loading',
37 | component: resolve => require(['./features/loading.vue'], resolve),
38 | meta: {
39 | title: '数据加载状态',
40 | },
41 | },
42 | {
43 | path: '/sortable',
44 | name: 'sortable',
45 | component: resolve => require(['./features/sortable.vue'], resolve),
46 | meta: {
47 | title: '排序',
48 | },
49 | },
50 | {
51 | path: '/render',
52 | name: 'render',
53 | component: resolve => require(['./features/render.vue'], resolve),
54 | meta: {
55 | title: 'Render函数',
56 | },
57 | },
58 | {
59 | path: '/fixedLeft',
60 | name: 'fixedLeft',
61 | component: resolve => require(['./features/fixedLeft.vue'], resolve),
62 | meta: {
63 | title: '固定列',
64 | },
65 | },
66 | {
67 | path: '/fixedLeft1',
68 | name: 'fixedLeft1',
69 | component: resolve => require(['./features/fixedLeft1.vue'], resolve),
70 | meta: {
71 | title: '固定列',
72 | },
73 | },
74 | {
75 | path: '/resizable',
76 | name: 'resizable',
77 | component: resolve => require(['./features/resizable.vue'], resolve),
78 | meta: {
79 | title: '调整宽度',
80 | },
81 | },
82 | {
83 | path: '/selectable',
84 | name: 'selectable',
85 | component: resolve => require(['./features/selectable.vue'], resolve),
86 | meta: {
87 | title: '多选/全屏固定头部',
88 | },
89 | },
90 | {
91 | path: '/expand',
92 | name: 'expand',
93 | component: resolve => require(['./features/expand.vue'], resolve),
94 | meta: {
95 | title: '展开',
96 | },
97 | },
98 | {
99 | path: '/scopedSlot',
100 | name: 'scopedSlot',
101 | component: resolve => require(['./features/scopedSlot.vue'], resolve),
102 | meta: {
103 | title: 'scoped-slot',
104 | },
105 | },
106 | {
107 | path: '/style',
108 | name: 'style',
109 | component: resolve => require(['./features/style.vue'], resolve),
110 | meta: {
111 | title: '风格样式',
112 | },
113 | },
114 | {
115 | path: '/addColor',
116 | name: 'addColor',
117 | component: resolve => require(['./features/addColor.vue'], resolve),
118 | meta: {
119 | title: '初始渲染/勾选渲染',
120 | },
121 | },
122 | {
123 | path: '/span',
124 | name: 'span',
125 | component: resolve => require(['./features/span.vue'], resolve),
126 | meta: {
127 | title: '合并',
128 | },
129 | },
130 | ];
131 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import FlexTable from './src/table.vue';
2 |
3 | const install = function (Vue) {
4 | Vue.component('flex-table', FlexTable);
5 | };
6 |
7 | if (typeof window !== 'undefined' && window.Vue) {
8 | install(window.Vue);
9 | }
10 |
11 | export default { install, FlexTable };
12 | export { install, FlexTable };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tm-flextable",
3 | "version": "1.2.17",
4 | "description": "Using div to implement flexible、high performance table",
5 | "main": "dist/index.js",
6 | "typings": "types/index.d.ts",
7 | "scripts": {
8 | "dev": "cross-env NODE_ENV=development webpack-dev-server --content-base test/ --open --inline --hot --compress --history-api-fallback --port 8086 --config build/webpack.dev.config.js",
9 | "demo": "cross-env NODE_ENV=production webpack --config build/webpack.demo.config.js",
10 | "dist": "cross-env NODE_ENV=production webpack --config build/webpack.dist.config.js",
11 | "test": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
12 | "coverage": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
13 | "report-coverage": "codecov",
14 | "lint": "eslint --fix ./src",
15 | "prepare": "npm run dist"
16 | },
17 | "files": [
18 | "dist",
19 | "src",
20 | "types",
21 | "index.js"
22 | ],
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/tm-fe/FlexTable.git"
26 | },
27 | "keywords": [
28 | "flextable",
29 | "div table",
30 | "vue table",
31 | "table"
32 | ],
33 | "author": "tm",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/tm-fe/FlexTable/issues"
37 | },
38 | "homepage": "https://github.com/tm-fe/FlexTable#readme",
39 | "dependencies": {
40 | "lodash.debounce": "^4.0.8",
41 | "lodash.throttle": "^4.1.1",
42 | "normalize-wheel": "^1.0.1",
43 | "vue-checkbox-radio": "^0.6.0"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.4.4",
47 | "@babel/plugin-transform-runtime": "^7.4.4",
48 | "@babel/preset-env": "^7.4.4",
49 | "@babel/preset-stage-3": "^7.0.0",
50 | "@babel/runtime": "^7.4.4",
51 | "@types/chai": "^4.1.7",
52 | "@types/mocha": "^5.2.7",
53 | "@vue/test-utils": "^1.0.0-beta.29",
54 | "autoprefixer-loader": "^3.2.0",
55 | "babel-eslint": "^10.0.1",
56 | "babel-loader": "^8.0.5",
57 | "babel-plugin-istanbul": "^5.1.4",
58 | "babel-polyfill": "^6.26.0",
59 | "chai": "^4.2.0",
60 | "cross-env": "^5.2.0",
61 | "css-loader": "^2.1.1",
62 | "eslint": "^5.16.0",
63 | "eslint-config-airbnb": "^17.1.0",
64 | "eslint-plugin-import": "^2.17.2",
65 | "eslint-plugin-jsx-a11y": "^6.2.1",
66 | "eslint-plugin-react": "^7.13.0",
67 | "eslint-plugin-vue": "^5.2.2",
68 | "friendly-errors-webpack-plugin": "^1.7.0",
69 | "html-loader": "^0.5.5",
70 | "html-webpack-plugin": "^3.2.0",
71 | "karma": "^4.1.0",
72 | "karma-chrome-launcher": "^2.2.0",
73 | "karma-coverage": "^1.1.2",
74 | "karma-mocha": "^1.3.0",
75 | "karma-sinon-chai": "^2.0.2",
76 | "karma-sourcemap-loader": "^0.3.7",
77 | "karma-spec-reporter": "^0.0.32",
78 | "karma-webpack": "^3.0.5",
79 | "less": "^3.9.0",
80 | "less-loader": "^5.0.0",
81 | "mocha": "^6.1.4",
82 | "pre-commit": "^1.2.2",
83 | "sass-loader": "^7.1.0",
84 | "sinon": "^7.3.2",
85 | "sinon-chai": "^3.3.0",
86 | "style-loader": "^0.23.1",
87 | "ts-loader": "^6.0.2",
88 | "typescript": "^3.5.1",
89 | "uglifyjs-webpack-plugin": "^2.1.2",
90 | "url-loader": "^1.1.2",
91 | "vue": "^2.6.10",
92 | "vue-loader": "^15.7.0",
93 | "vue-router": "^3.0.6",
94 | "vue-style-loader": "^4.1.2",
95 | "vue-template-compiler": "^2.6.10",
96 | "webpack": "^4.30.0",
97 | "webpack-cli": "^3.3.2",
98 | "webpack-dev-server": "^3.3.1",
99 | "webpack-merge": "^4.2.1"
100 | },
101 | "pre-commit": [
102 | "lint"
103 | ]
104 | }
105 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # FlexTable
2 | [![NPM version][npm-image]][npm-url]
3 | [![build status][travis-image]][travis-url]
4 | [![npm download][download-image]][download-url]
5 | [![codecov][codecov-image]](codecov-url)
6 |
7 | [npm-image]: http://img.shields.io/npm/v/tm-flextable.svg?style=flat-square
8 | [npm-url]: http://npmjs.org/package/tm-flextable
9 | [travis-image]: https://img.shields.io/travis/tm-fe/FlexTable.svg?style=flat-square
10 | [travis-url]: https://travis-ci.org/tm-fe/FlexTable
11 | [download-image]: https://img.shields.io/npm/dm/tm-flextable.svg?style=flat-square
12 | [download-url]: https://npmjs.org/package/tm-flextable
13 | [codecov-image]: https://codecov.io/gh/tm-fe/FlexTable/branch/master/graph/badge.svg
14 | [codecov-url]: https://codecov.io/gh/tm-fe/FlexTable
15 |
16 | An efficiently updated div table Vue component. Compatible with Vue 2.x
17 |
18 | - [Why div table?](#why-div-table)
19 | - [Screenshots](#screenshots)
20 | - [Feature](#feature)
21 | - [Install](#install)
22 | - [Usage](#usage)
23 | - [API](#api)
24 | - [Demo](#demo)
25 |
26 | ## Demo
27 | To view a demo online: [https://tm-fe.github.io/FlexTable/examples/dist/](https://tm-fe.github.io/FlexTable/examples/dist/)
28 |
29 | To view demo examples locally clone the repo and run `yarn install && yarn dev` or view [local example](./examples)
30 |
31 | ## Screenshots
32 |
33 | 
34 |
35 | ## Feature
36 |
37 | - [x] 支持最大高度,超过 fixed header
38 | - [x] 固定列
39 | - [x] footer 展示汇总数据
40 | - [x] 自定义列宽
41 | - [x] 排序
42 | - [x] 拖动调整列宽(resizable)
43 | - [x] selectable
44 | - [x] expand 嵌套功能
45 | - [x] 异步渲染
46 | - [x] selectable模式下渲染选中行背景色
47 | - [x] 初始化渲染行、列、单元格背景色
48 | - [ ] 合并单元格
49 | - [ ] 拖动改变列顺序
50 |
51 | ## Install
52 |
53 | ```bash
54 | npm install --save tm-flextable
55 | // or
56 | yarn add tm-flextable
57 | ```
58 | ```js
59 | import { FlexTable } from 'tm-flextable';
60 |
61 | export default {
62 | // ...
63 | components: {
64 | FlexTable
65 | }
66 | // ...
67 | }
68 | ```
69 |
70 | ## Usage
71 |
72 | ### CDN 引入
73 | ```html
74 |
75 | ```
76 | 然后直接在页面使用
77 | ```html
78 |
79 |
85 |
86 |
87 |
98 | ```
99 |
100 | ### npm 安装(推荐)
101 | ```js
102 | // main.js
103 | import Vue from 'vue';
104 | import VueRouter from 'vue-router';
105 | import App from 'components/app.vue';
106 | import Routers from './router.js';
107 | import FlexTable from 'tm-flextable';
108 |
109 | Vue.use(VueRouter);
110 | Vue.use(FlexTable); // 全局注册组件
111 |
112 | //or
113 | // app.vue
114 | // 局部注册
115 | import { FlexTable } from 'tm-flextable';
116 | export default {
117 | components:{
118 | flexTable
119 | },
120 | // ...
121 |
122 | ```
123 |
124 | ## API
125 |
126 | ### Table props
127 |
128 | | 属性 | 说明 | 类型 | 默认值 |
129 | | ------------ | ------- | ------- | ----------- |
130 | | data | 显示的结构化数据 | Array | [] |
131 | | columns | 表格列的配置描述,具体项见后文 | Array | [] |
132 | | sum | 显示的结构化数据汇总 | Object | {} |
133 | | loading | 是否加载中 | Boolean | false |
134 | | resizable | 是否可拖动调整列宽 | Boolean | false |
135 | | height | 表格高度,单位 px,设置后,如果表格内容大于此值,会固定表头 | Number | - |
136 | | no-data | 数据为空时显示的提示内容 | String | No Data |
137 | | asyncRender | 不为 0 时使用异步渲染模式,mounted 触发前渲染的行数(建议是刚好首屏,**见后文详细说明**) | number | 0 |
138 | | minWidth | 最小列宽 | Number | 40 |
139 | | maxWidth | 拖动调整时,可调的最大列宽, 默认不限制 | number | - |
140 | | size | 表格大小 default/big/small | String | default |
141 | | theme | 颜色 light/dark | String | light |
142 | | border | 边框显示 | Boolean | true |
143 | | stripe | 行的斑纹显示 | Boolean | true |
144 | | fixedHead | 全屏固定头部 | Boolean | false |
145 | | fixedHeadTop | 全屏固定头部离顶部距离 | Number | 0 |
146 | | checkFixedHeadTop | 全屏固定头部离顶部距离判断(可以自定义) | Function | '' |
147 | | selectedClass | 单选或多选模式下,渲染选中行样式 | string | '' |
148 | | rowClassName | 初始化渲染行背景色 | Function | '' |
149 | | autoCalWidth | 是否自动计算width | Boolean | true, 默认true,false时会严格按照设置的width来展示 |
150 | | span-method | 合并行(合并列暂未实现) | Function | 方法的参数是一个对象,里面包含当前行row、当前列column、当前行号rowIndex、当前列号columnIndex四个属性。该函数可以返回一个包含两个元素的数组,第一个元素代表rowspan,第二个元素代表colspan。 也可以返回一个键名为rowspan和colspan的对象。具体见 demo |
151 | | multiple | 是否多选(设置false即为单选) | Boolean | true |
152 | | selectedData | 传入的默认选中id | Array | |
153 | | uniqueKey | 表格数据的唯一值名称(处理id重复报错问题) | String | |
154 | | virtualScroll | 虚拟滚动的展示条数(设置此值即自动开启虚拟滚动功能) | Number | |
155 | | virtualHeight | 虚拟滚动的单条数据高度(开启虚拟滚动时必填,否则表格会有间隙错位)| Number | 40 |
156 | | scrollContainer | 表格所在的滚动容器,默认document,传String会使用document.querySelector查询 | String/Object | document |
157 | | fixedXScroll | 是否固定横向滚动 | Boolean | false |
158 | | fixedXScrollBottom | 固定横向滚动的底部位置 | Number/String | 0 |
159 | | vertical | 表格单元格是否垂直居中 | Boolean | false(如果需要某一列需要垂直居中:在表columns中给需要垂直居中的字段增加 vertical: true 即可) |
160 |
161 |
162 | ### Table events
163 |
164 | | 事件名 | 说明 | 返回值 |
165 | | ------------ | ------- | ----------- |
166 | | on-sort-change | 排序时有效,当点击排序时触发 | column:当前列数据; key:排序依据的指标; order:排序的顺序,值为 asc 或 desc |
167 | | on-selection-change | 点击全选时触发 | selection:已选项数据; row: 当前选中行数据 |
168 | | on-all-cancel | 全选取消时触发 | selection:已选项数据 |
169 | | on-selection-cancel | 单选取消时触发 | selection:已选项数据 |
170 | | on-render-done | 异步渲染完成时触发(asyncRender 不为 0 时生效) | - |
171 | | on-scroll-x | 横向滚动事件 | event |
172 | | on-col-width-resize | 调整列宽事件 | newWidth, oldWidth, column, event |
173 |
174 | ### column
175 | 列描述数据对象,是 columns 中的一项
176 |
177 | | 属性 | 说明 | 类型 | 默认值 |
178 | | ------------ | ------- | ------- | ----------- |
179 | | title | 列名 | String | - |
180 | | key | 列名 | String | - |
181 | | type | 列类型,可选值为 index、selection、expand | String | - |
182 | | width | 列宽,不设置将自动分配,最小 60px | Number | 60 |
183 | | align | 对齐方式,可选值为 left 左对齐、right 右对齐和 center 居中对齐 | String | Left |
184 | | fixed | 列是否固定在左侧或者右侧,可选值为 `left`、`right` | String | - |
185 | | render | 自定义渲染列,使用 Vue 的 Render 函数。传入两个参数,第一个是 h,第二个为对象,包含 row、column 和 index,分别指当前行数据,当前列数据,当前行索引,详见示例。 | Function | - |
186 | | renderHeader | 自定义列头显示内容,使用 Vue 的 Render 函数。传入两个参数,第一个是 h,第二个为对象,包含 column 和 index,分别为当前列数据和当前列索引。 | Function | - |
187 | | sortable | 对应列是否可以排序,如果设置为 custom,则代表用户希望远程排序,需要监听 Table 的 on-sort-change 事件 | Boolean | false |
188 | | sortType | 设置初始化排序。值为 asc, desc 和 normal | String | normal |
189 | | resizable | 是否可拖动调整列宽(必须设置table props 的 resizable 为 true 才生效) | Boolean | - |
190 | | minWidth | 最小列宽(优先级高于table props) | number | - |
191 | | maxWidth | 拖动调整时,可调的最大列宽, 默认不限制(优先级高于table props) | number | - |
192 | | className | 初始化渲染列的背景色 | string | '' |
193 |
194 | ### data
195 | 行描述数据对象,是 list 中的一项
196 |
197 | | 属性 | 说明 | 类型 | 默认值 |
198 | | ------------ | ------- | ------- | ----------- |
199 | | cellClassName | 指定任意一个单元格的背景色 | Object | {} |
200 |
201 | ### 特别说明
202 | 行类名、列类名、单元格类名和选中行类名的权重由它们的定义顺序决定
203 | 定义在后面的权重相对较大
204 |
205 | ### Table slot
206 |
207 | | 名称 | 说明 |
208 | | ------------ | ------- |
209 | | loading | 加载中 |
210 |
211 | ## asyncRender
212 |
213 | **异步渲染功能,适用于数据量特别大,改善首次渲染慢的情况。asyncRender 值为 mounted 之前首次渲染的行数,剩余行数会在 mounted 之后以 RAF 的方式逐行渲染,因此如果没有设置表格最大高度 height, 可能会造成页面抖动和 reflow, 建议设置 table height prop。 此外, 当表格数据 data 属性变化时,也会造成整表重新渲染,而失去 vue diff 的优势, 可以在首次异步渲染完成后的 on-render-done 事件中,将 asyncRender 的值改为 pageSize 相同的值,这样可以避免整表重新渲染。**
214 |
215 | ## virtualScroll
216 |
217 | 
218 |
219 | 虚拟滚动功能注意点:
220 |
221 | - 1. 只支持每条数据高度一致的情况,不支持展开行以及任何会改变单元格高度的方式;
222 | - 2. 不支持分组表头;
223 | - 3. 当表格有固定列的情况,虚拟滚动可能会有延迟
224 | - 4. 必须给源数据每一项加上唯一id。必须确定且唯一。假如用随机数,会导致每次的id都不一致,vue会误以为是有是数据更新,无法复用。详见Vue的dIff算法。
225 |
226 | ## Test
227 | ```bash
228 | yarn test
229 | or
230 | npm test
231 | ```
232 |
233 | ## Coverage
234 |
235 | ## License
236 | `tm-flextable` is released under the MIT license.
237 |
--------------------------------------------------------------------------------
/src/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
37 |
65 |
--------------------------------------------------------------------------------
/src/data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview 定义的数据常量
3 | */
4 |
5 | module.exports = {
6 | /**
7 | * 单元格最小宽度
8 | * @type {Number}
9 | */
10 | MIN_WIDTH: 40,
11 | };
12 |
--------------------------------------------------------------------------------
/src/expand.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'TableExpand',
3 | functional: true,
4 | props: {
5 | class: String,
6 | row: Object,
7 | render: Function,
8 | index: Number,
9 | column: {
10 | type: Object,
11 | default: null,
12 | },
13 | },
14 | render: (h, ctx) => {
15 | const params = {
16 | row: ctx.props.row,
17 | index: ctx.props.index,
18 | };
19 | if (ctx.props.column) params.column = ctx.props.column;
20 | return ctx.props.render(h, params);
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/mixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | calWidth: {
4 | type: String,
5 | required: true,
6 | },
7 | },
8 | computed: {
9 | owner() {
10 | let parent = this.$parent;
11 | while (parent && !parent.tableId) {
12 | parent = parent.$parent;
13 | }
14 | return parent;
15 | },
16 | calWidthObj() {
17 | return JSON.parse(this.calWidth);
18 | },
19 | },
20 | methods: {
21 | setCellStyle(column, type) {
22 | const sWidth = this.calWidthObj[column.key];
23 | const oStyle = {};
24 | if (sWidth) {
25 | oStyle.width = `${sWidth}px`;
26 | oStyle.flex = 'none';
27 | }
28 | if (column.align) {
29 | oStyle['text-align'] = column.align;
30 | }
31 | if (type !== 'head') {
32 | oStyle.display = 'grid';
33 | oStyle['align-items'] = 'center';
34 | }
35 |
36 | return oStyle;
37 | },
38 | alignCls(column, row = {}) {
39 | let cellClassName = '';
40 | if (row.cellClassName && column.key && row.cellClassName[column.key]) {
41 | cellClassName = row.cellClassName[column.key];
42 | }
43 | return [
44 | {
45 | [`${cellClassName}`]: cellClassName, // cell className
46 | [`${column.className}`]: column.className, // column className
47 | },
48 | ];
49 | },
50 | handleWidth(curColumn, oriColumn) {
51 | const idx = this.columns.findIndex(
52 | item => item.key === curColumn.key
53 | && curColumn.fixed === 'left'
54 | && curColumn.type !== 'selection',
55 | );
56 | const beforeKey = JSON.parse(JSON.stringify(oriColumn))
57 | .splice(0, idx)
58 | .map(item => item.key);
59 | let num = 0;
60 | beforeKey.forEach((item) => {
61 | num += this.calWidthObj[item];
62 | });
63 | if (num) {
64 | return {
65 | left: `${num}px`,
66 | };
67 | }
68 | },
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/src/slot.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'TableSlot',
3 | functional: true,
4 | props: {
5 | class: String,
6 | row: Object,
7 | index: Number,
8 | column: {
9 | type: Object,
10 | default: null,
11 | },
12 | owner: Object,
13 | type: {
14 | type: String,
15 | default: 'body',
16 | },
17 | },
18 | render: (h, ctx) => {
19 | const { column } = ctx.props;
20 | const { key } = column;
21 | const classDefault = `flex-table-slot-${ctx.props.type}`;
22 | const className = [
23 | classDefault,
24 | `${classDefault}-${key}`,
25 | ];
26 | return h('div', {
27 | class: className,
28 | }, ctx.props.owner.$scopedSlots[key]({
29 | row: ctx.props.row,
30 | column,
31 | index: ctx.props.index,
32 | type: ctx.props.type,
33 | key,
34 | }));
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/src/tableFoot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
37 |
110 |
--------------------------------------------------------------------------------
/src/tableLoadingBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
105 |
133 |
--------------------------------------------------------------------------------
/src/tableScrollBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
51 |
--------------------------------------------------------------------------------
/src/tableSum.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
21 |
22 |
30 |
31 |
32 | {{ headSum[item.key] }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
159 |
187 |
--------------------------------------------------------------------------------
/src/tableTr.vue:
--------------------------------------------------------------------------------
1 |
2 |
28 |
29 |
143 |
--------------------------------------------------------------------------------
/test/index.d.ts:
--------------------------------------------------------------------------------
1 |
2 | interface FlexTableColumnOption {
3 | [index: string]: string | number | boolean;
4 | name: string;
5 | age: number;
6 | address: string;
7 | date: string;
8 | }
9 |
10 | interface FlexTableRow {
11 | row: FlexTableColumnOption;
12 | column: {
13 | title: string;
14 | };
15 | }
16 |
17 | interface SortOption {
18 | key: string;
19 | order: string;
20 | }
21 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "noImplicitAny": false,
13 | "sourceMap": true,
14 | "baseUrl": ".",
15 | "types": [
16 | "mocha",
17 | "vue"
18 | ],
19 | "paths": {
20 | "@/*": [
21 | "unit/*"
22 | ]
23 | },
24 | "lib": [
25 | "esnext",
26 | "dom",
27 | ]
28 | },
29 | "include": [
30 | "**/*.ts",
31 | ],
32 | "exclude": [
33 | "node_modules"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/test/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warning",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "linterOptions": {
7 | "exclude": [
8 | "node_modules/**"
9 | ]
10 | },
11 | "rules": {
12 | "quotemark": [true, "single"],
13 | "indent": [true, "spaces", 4],
14 | "interface-name": false,
15 | "ordered-imports": false,
16 | "object-literal-sort-keys": false,
17 | "no-consecutive-blank-lines": false,
18 | "no-namespace": false,
19 | "only-arrow-functions": false
20 | }
21 | }
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | const testsContext = require.context('./specs', true, /\.spec$/);
2 | testsContext.keys().forEach(testsContext);
3 |
4 | const srcContext = require.context('../../src', true, /^\.\/(?!.*(\.less)?$)/);
5 | srcContext.keys().forEach(srcContext);
6 |
--------------------------------------------------------------------------------
/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpackConfig = require('../../build/webpack.test.config');
2 |
3 | module.exports = function(config) {
4 | const configuration = {
5 | browsers: ['ChromeHeadless'],
6 | frameworks: ['mocha', 'sinon-chai'],
7 | reporters: ['spec', 'coverage'],
8 | files: [
9 | 'index.js',
10 | ],
11 | preprocessors: {
12 | 'index.js': ['webpack', 'sourcemap'],
13 | },
14 | webpack: webpackConfig,
15 | webpackMiddleware: {
16 | noInfo: true,
17 | },
18 | // optionally, configure the reporter
19 | coverageReporter: {
20 | dir: '../../coverage',
21 | reporters: [
22 | { type: 'lcov', subdir: '.' },
23 | { type: 'text-summary' },
24 | { type: 'cobertura', subdir: '.' },
25 | { type: 'json', subdir: '.' },
26 | ],
27 | },
28 | client: {
29 | mocha: {
30 | timeout: 4000,
31 | },
32 | },
33 | };
34 |
35 | config.set(configuration);
36 | };
37 |
--------------------------------------------------------------------------------
/test/unit/specs/expand.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createVue,
3 | // destroyVM,
4 | triggerEvent,
5 | waitImmediate,
6 | } from '@/util';
7 | import { expect } from 'chai';
8 |
9 | const aTestList: FlexTableColumnOption[] = [];
10 | for (let i = 0; i < 1; i += 1) {
11 | const oTestData = {
12 | name: 'John Brown',
13 | age: 18,
14 | address: 'New York No. 1 Lake Park',
15 | date: '2016-10-03',
16 | };
17 | aTestList.push(oTestData);
18 | }
19 |
20 |
21 | describe('Flex-Table', () => {
22 | // 基础测试
23 | describe('expand', () => {
24 | const vm = createVue({
25 | template: `
26 |
33 | `,
34 | data() {
35 | return {
36 | columns: [
37 | {
38 | type: 'expand',
39 | width: 50,
40 | render: (h: any, params: FlexTableRow) => {
41 | return h('p', {}, params.row.name);
42 | },
43 | },
44 | {
45 | title: 'Name',
46 | key: 'name',
47 | },
48 | {
49 | title: 'Age',
50 | key: 'age',
51 | render(h: any, params: FlexTableRow) {
52 | return h('span', `age: ${params.row.age}`);
53 | },
54 | },
55 | {
56 | title: 'Address',
57 | key: 'address',
58 | },
59 | {
60 | title: 'Date',
61 | key: 'date',
62 | },
63 | ],
64 | loading: false,
65 | list: aTestList,
66 | sum: {
67 | name: 'Jim Green',
68 | age: 24,
69 | address: 'London',
70 | date: '2016-10-01',
71 | },
72 | height: 250,
73 | };
74 | },
75 | });
76 | const elemExpandBtn = vm.$el.querySelector('.flex-table-col-icon');
77 | // 检测
78 | it('check expand', async () => {
79 | let elemNextHtml = '';
80 | if (elemExpandBtn) {
81 | triggerEvent(elemExpandBtn, 'click');
82 | await waitImmediate();
83 | if ( elemExpandBtn.parentElement ) {
84 | const elemNext = elemExpandBtn.parentElement.nextElementSibling;
85 | if ( elemNext && elemNext.innerHTML) {
86 | elemNextHtml = elemNext.innerHTML;
87 | }
88 | }
89 | }
90 |
91 | expect(elemNextHtml).to.eql('John Brown
');
92 | });
93 |
94 | it('check unexpanded', async () => {
95 | triggerEvent(elemExpandBtn, 'click');
96 | await waitImmediate();
97 | let elemNext;
98 | if (elemExpandBtn && elemExpandBtn.parentElement){
99 | elemNext = elemExpandBtn.parentElement.nextElementSibling;
100 | }
101 |
102 | expect(elemNext).to.eql(null);
103 | });
104 |
105 | // destroyVM(vm); // 这里不用销毁方法,因为点击后出发vue的修改,如果销毁了vm,则获取dom有误
106 | });
107 | describe('expand scoped slot', () => {
108 | const vm = createVue({
109 | template: `
110 |
117 |
118 | {{ row.name }}
119 |
120 |
121 | `,
122 | data() {
123 | return {
124 | columns: [
125 | {
126 | type: 'expand',
127 | width: 50,
128 | },
129 | {
130 | title: 'Name',
131 | key: 'name',
132 | },
133 | {
134 | title: 'Age',
135 | key: 'age',
136 | render(h, params) {
137 | return h(
138 | 'span',
139 | `age: ${params.row.age}`,
140 | );
141 | },
142 | },
143 | {
144 | title: 'Address',
145 | key: 'address',
146 | },
147 | {
148 | title: 'Date',
149 | key: 'date',
150 | },
151 | ],
152 | loading: false,
153 | list: aTestList,
154 | sum: {
155 | name: 'Jim Green',
156 | age: 24,
157 | address: 'London',
158 | date: '2016-10-01',
159 | },
160 | height: 250,
161 | };
162 | }
163 | });
164 | const elemExpandBtn = vm.$el.querySelector('.flex-table-col-icon');
165 | // 检测
166 | it('check expand', async () => {
167 | triggerEvent(elemExpandBtn, 'click');
168 | await waitImmediate();
169 | let elemNext;
170 | if (elemExpandBtn && elemExpandBtn.parentElement) {
171 | elemNext = elemExpandBtn.parentElement.nextElementSibling;
172 | }
173 | expect(elemNext.innerHTML).to.eql('');
174 | });
175 |
176 | it('check unexpanded', async () => {
177 | triggerEvent(elemExpandBtn, 'click');
178 | await waitImmediate();
179 | let elemNext;
180 | if (elemExpandBtn && elemExpandBtn.parentElement) {
181 | elemNext = elemExpandBtn.parentElement.nextElementSibling;
182 | }
183 | expect(elemNext).to.eql(null);
184 | });
185 | });
186 | });
187 |
--------------------------------------------------------------------------------
/test/unit/specs/fixed.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createVue,
3 | // destroyVM,
4 | // triggerEvent,
5 | waitImmediate,
6 | wait
7 | } from '@/util';
8 | import { expect } from 'chai';
9 | import Vue from 'vue';
10 |
11 | const aTestList: FlexTableColumnOption[] = [];
12 | for (let i = 0; i < 2; i += 1) {
13 | const oTestData = {
14 | name: 'John Brown',
15 | age: 18,
16 | address: 'New York No. 1 Lake Park',
17 | date: '2016-10-03',
18 | };
19 | aTestList.push(oTestData);
20 | }
21 | function checkFixedLayout(vm: Vue, type: string) {
22 | const aFixedTable = vm.$el.querySelectorAll(`.flex-table-fixed-${type}`);
23 | expect(aFixedTable.length).to.eql(1);
24 | }
25 |
26 | function checkLayoutHead(vm: Vue, type: string, i: number) {
27 | const aFixedTableHeadCol = vm.$el.querySelectorAll(`.flex-table-fixed-${type} .flex-table-head .flex-table-col`);
28 | let bCheck = true;
29 | aFixedTableHeadCol.forEach((element: any, index: number) => {
30 | // 如果不是第2列,并且存在内容,则表示渲染失败
31 | if (index !== i && element.innerText) {
32 | bCheck = false;
33 | }
34 | });
35 | expect(bCheck).to.eql(true);
36 | }
37 |
38 | describe('Flex-Table', () => {
39 | // 基础测试
40 | describe('fixed', () => {
41 | const vm: Vue = createVue({
42 | template: `
43 |
51 | `,
52 | data() {
53 | return {
54 | columns: [
55 | {
56 | title: 'Name',
57 | key: 'name',
58 | width: 100,
59 | fixed: 'left',
60 | },
61 | {
62 | title: 'Age',
63 | key: 'age',
64 | width: 100,
65 | fixed: 'right',
66 | render(h: Vue.CreateElement, params: FlexTableRow ) {
67 | return h('span', `age: ${params.row.age}`);
68 | },
69 | },
70 | {
71 | title: 'Address',
72 | key: 'address',
73 | },
74 | {
75 | title: 'Date',
76 | key: 'date',
77 | },
78 | ],
79 | loading: false,
80 | list: aTestList,
81 | sum: {
82 | name: 'Jim Green',
83 | age: 24,
84 | address: 'London',
85 | date: '2016-10-01',
86 | },
87 | height: 0,
88 | };
89 | },
90 | });
91 |
92 | // 检测 是否生成了fixed层
93 | it('check fixed-left layout', (done) => {
94 | checkFixedLayout(vm, 'left');
95 | done();
96 | });
97 |
98 | it('check fixed-right layout', (done) => {
99 | checkFixedLayout(vm, 'right');
100 | done();
101 | });
102 |
103 | // 检测 fixed层的head是否符合
104 | it('check fixed-left layout-head', (done) => {
105 | checkLayoutHead(vm, 'left', 0);
106 | done();
107 | });
108 | it('check fixed-right layout-head', (done) => {
109 | checkLayoutHead(vm, 'right', vm.$data.columns.length - 1);
110 | done();
111 | });
112 |
113 | // 检测 fiexed header
114 | it('check fixed-header base', async () => {
115 | vm.$data.height = 250;
116 | await wait(20);
117 | let bCheck = false;
118 | const elemBody = vm.$el.querySelector('.flex-table-body');
119 |
120 | if (elemBody && elemBody.classList) {
121 | bCheck = elemBody.classList.contains('flex-table-fixed-header');
122 | }
123 |
124 | expect(bCheck).to.eql(true);
125 | });
126 | it('check fixed-header height', async () => {
127 | const nHeight = 250;
128 | vm.$data.height = nHeight;
129 | await waitImmediate();
130 | let nMaxHeight = 0;
131 | const elemBody = vm.$el.querySelector('.flex-table-body') as HTMLElement;
132 |
133 | if (elemBody && elemBody.style && elemBody.style.maxHeight) {
134 | nMaxHeight = Number(elemBody.style.maxHeight.replace('px', ''));
135 | }
136 |
137 | expect(nMaxHeight).to.eql(nHeight);
138 | });
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/test/unit/specs/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { createVue } from '@/util';
3 | import { setFoot } from '@/tool';
4 |
5 | const aTestList: FlexTableColumnOption[] = [];
6 | const aTestData: string[] = [];
7 | for (let i = 0; i < 10; i++) {
8 | const oTestData: FlexTableColumnOption = {
9 | name: 'John Brown',
10 | age: 18,
11 | address: 'New York No. 1 Lake Park',
12 | date: '2016-10-03',
13 | };
14 | aTestList.push(oTestData);
15 | Object.keys(oTestData).forEach((k) => {
16 | aTestData.push(oTestData[k].toString());
17 | });
18 | }
19 | describe('Flex-Table', () => {
20 | // 基础测试
21 | describe('base', () => {
22 | const vm = createVue({
23 | template: `
24 |
31 | `,
32 | data() {
33 | return {
34 | columns: [
35 | {
36 | title: 'Name',
37 | key: 'name',
38 | },
39 | {
40 | title: 'Age',
41 | key: 'age',
42 | },
43 | {
44 | title: 'Address',
45 | key: 'address',
46 | },
47 | {
48 | title: 'Date',
49 | key: 'date',
50 | },
51 | ],
52 | loading: false,
53 | list: aTestList,
54 | sum: {
55 | name: 'Jim Green',
56 | age: 24,
57 | address: 'London',
58 | date: '2016-10-01',
59 | },
60 | };
61 | },
62 | });
63 |
64 |
65 | // 检测头部
66 | it('check head', (done) => {
67 | const aHead = vm.$el.querySelectorAll('.flex-table-head .flex-table-col>span');
68 | const aHeadTitle: string[] = [];
69 | aHead.forEach(function(node) {
70 | if (node && node.textContent) {
71 | aHeadTitle.push(node.textContent.trim());
72 | }
73 | });
74 | expect(aHeadTitle).to.eql(['Name', 'Age', 'Address', 'Date']);
75 | done();
76 | });
77 |
78 | // 检测 输入的内容
79 | it('check body', (done) => {
80 | const aBodyRow = vm.$el.querySelectorAll('.flex-table-body .flex-table-row');
81 | const aBodyData: string[] = [];
82 | aBodyRow.forEach( (node) => {
83 | const aCol = node.querySelectorAll('.flex-table-col');
84 | aCol.forEach( (elem) => {
85 | if (elem && elem.textContent) {
86 | aBodyData.push(elem.textContent.trim());
87 | }
88 | });
89 | });
90 | expect(aBodyData).to.eql(aTestData);
91 | done();
92 | });
93 |
94 | // 检测 汇总信息
95 | it('check sum', (done) => {
96 | const aFootRow = vm.$el.querySelectorAll('.flex-table-foot .flex-table-row .flex-table-col');
97 | const aFootLabel: string[] = [];
98 | const aFootValue: string[] = [];
99 | aFootRow.forEach( (node) => {
100 | const aDoms = node.querySelectorAll('p');
101 | setFoot(aDoms, aFootValue, aFootLabel);
102 | });
103 | expect(aFootValue).to.eql(['Jim Green', '24', 'London', '2016-10-01']);
104 | expect(aFootLabel).to.eql(['Name', 'Age', 'Address', 'Date']);
105 | done();
106 | });
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/test/unit/specs/initRowNumber.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { createVue } from '@/util';
3 | import { setFoot } from '@/tool';
4 | import { expect } from 'chai';
5 | import Vue from 'vue';
6 |
7 | const aTestList: FlexTableColumnOption[] = [];
8 | const aTestData: string[] = [];
9 | for (let i = 0; i < 10; i += 1) {
10 | const oTestData: FlexTableColumnOption = {
11 | name: 'John Brown',
12 | age: 18,
13 | address: 'New York No. 1 Lake Park',
14 | date: '2016-10-03',
15 | };
16 | aTestList.push(oTestData);
17 | Object.keys(oTestData).forEach((k) => {
18 | const sValue = oTestData[k].toString();
19 | if (k === 'age') {
20 | aTestData.push(`age: ${sValue}`);
21 | } else {
22 | aTestData.push(sValue);
23 | }
24 | });
25 | }
26 |
27 | describe('Flex-Table', () => {
28 | describe('asyncRender', () => {
29 | const vm: Vue = createVue({
30 | template: `
31 |
39 | `,
40 | data() {
41 | return {
42 | columns: [
43 | {
44 | title: 'Name',
45 | key: 'name',
46 | renderHeader(h: Vue.CreateElement, params: FlexTableRow) {
47 | return h('span', `Custom Title : ${params.column.title}`);
48 | },
49 | width: 100,
50 | },
51 | {
52 | title: 'Age',
53 | key: 'age',
54 | render(h: Vue.CreateElement, params: FlexTableRow) {
55 | return h('span', `age: ${params.row.age}`);
56 | },
57 | width: 100,
58 | },
59 | {
60 | title: 'Address',
61 | key: 'address',
62 | width: 100,
63 | },
64 | {
65 | title: 'Date',
66 | key: 'date',
67 | width: 100,
68 | },
69 | ],
70 | loading: false,
71 | list: aTestList,
72 | sum: {
73 | name: 'Jim Green',
74 | age: 24,
75 | address: 'London',
76 | date: '2016-10-01',
77 | },
78 | asyncRender: 5,
79 | };
80 | },
81 | });
82 | // 检测 输入的内容
83 | it('check body', (done) => {
84 | const aBodyRow = vm.$el.querySelectorAll('.flex-table-body .flex-table-row');
85 | const aBodyData: string[] = [];
86 | aBodyRow.forEach((node) => {
87 | const aCol = node.querySelectorAll('.flex-table-col');
88 | aCol.forEach((elem) => {
89 | if (elem && elem.textContent) {
90 | aBodyData.push(elem.textContent.trim());
91 | }
92 | });
93 | });
94 | expect(aBodyData).to.eql(aTestData);
95 | done();
96 | });
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/test/unit/specs/loading.spec.ts:
--------------------------------------------------------------------------------
1 | import { createVue, waitImmediate } from '@/util';
2 | import { expect } from 'chai';
3 | import Vue from 'vue';
4 |
5 | const aTestList: FlexTableColumnOption[] = [];
6 | for (let i = 0; i < 5; i += 1) {
7 | const oTestData = {
8 | name: 'John Brown',
9 | age: 18,
10 | address: 'New York No. 1 Lake Park',
11 | date: '2016-10-03',
12 | };
13 | aTestList.push(oTestData);
14 | }
15 |
16 | describe('Flex-Table', () => {
17 | // 基础测试
18 | describe('loading', () => {
19 | const vm: Vue = createVue({
20 | template: `
21 |
26 | `,
27 | data() {
28 | return {
29 | columns: [
30 | {
31 | title: 'Name',
32 | key: 'name',
33 | },
34 | {
35 | title: 'Age',
36 | key: 'age',
37 | sortable: true,
38 | },
39 | {
40 | title: 'Address',
41 | key: 'address',
42 | },
43 | {
44 | title: 'Date',
45 | key: 'date',
46 | },
47 | ],
48 | loading: true,
49 | list: aTestList,
50 | };
51 | },
52 | methods: {},
53 | });
54 |
55 | // 检测 显示loading
56 | it('status:true', (done) => {
57 | const elemLoading = vm.$el.querySelector('.flex-table-spinner');
58 | expect(!!elemLoading).to.eql(true);
59 | done();
60 | });
61 |
62 | // 检测 取消loading
63 | it('status:false', async () => {
64 | vm.$data.loading = false;
65 | await waitImmediate();
66 | const elemLoading = vm.$el.querySelector('.flex-table-spinner');
67 | expect(!!elemLoading).to.eql(false);
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/test/unit/specs/render.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { createVue } from '@/util';
3 | import { setFoot } from '@/tool';
4 | import { expect } from 'chai';
5 | import Vue from 'vue';
6 |
7 | const aTestList: FlexTableColumnOption[] = [];
8 | const aTestData: string[] = [];
9 | for (let i = 0; i < 5; i += 1) {
10 | const oTestData: FlexTableColumnOption = {
11 | name: 'John Brown',
12 | age: 18,
13 | address: 'New York No. 1 Lake Park',
14 | date: '2016-10-03',
15 | };
16 | aTestList.push(oTestData);
17 | Object.keys(oTestData).forEach((k) => {
18 | const sValue = oTestData[k].toString();
19 | if (k === 'age') {
20 | aTestData.push(`age: ${sValue}`);
21 | } else {
22 | aTestData.push(sValue);
23 | }
24 | });
25 | }
26 |
27 | describe('Flex-Table', () => {
28 | // 基础测试
29 | describe('render', () => {
30 | const vm: Vue = createVue({
31 | template: `
32 |
39 | `,
40 | data() {
41 | return {
42 | columns: [
43 | {
44 | title: 'Name',
45 | key: 'name',
46 | renderHeader(h: Vue.CreateElement, params: FlexTableRow) {
47 | return h('span', `Custom Title : ${params.column.title}`);
48 | },
49 | },
50 | {
51 | title: 'Age',
52 | key: 'age',
53 | render(h: Vue.CreateElement, params: FlexTableRow) {
54 | return h('span', `age: ${params.row.age}`);
55 | },
56 | },
57 | {
58 | title: 'Address',
59 | key: 'address',
60 | },
61 | {
62 | title: 'Date',
63 | key: 'date',
64 | },
65 | ],
66 | loading: false,
67 | list: aTestList,
68 | sum: {
69 | name: 'Jim Green',
70 | age: 24,
71 | address: 'London',
72 | date: '2016-10-01',
73 | },
74 | };
75 | },
76 | });
77 |
78 | // 检测头部
79 | it('check head', (done) => {
80 | const aHead = vm.$el.querySelectorAll('.flex-table-head .flex-table-col>span');
81 | const aHeadTitle: string[] = [];
82 | aHead.forEach((node) => {
83 | if (node && node.textContent) {
84 | aHeadTitle.push(node.textContent);
85 | }
86 | });
87 | expect(aHeadTitle).to.eql(['Custom Title : Name', 'Age', 'Address', 'Date']);
88 | done();
89 | });
90 |
91 | // 检测 输入的内容
92 | it('check body', (done) => {
93 | const aBodyRow = vm.$el.querySelectorAll('.flex-table-body .flex-table-row');
94 | const aBodyData: string[] = [];
95 | aBodyRow.forEach((node) => {
96 | const aCol = node.querySelectorAll('.flex-table-col');
97 | aCol.forEach((elem) => {
98 | if (elem && elem.textContent) {
99 | aBodyData.push(elem.textContent.trim());
100 | }
101 | });
102 | });
103 | expect(aBodyData).to.eql(aTestData);
104 | done();
105 | });
106 |
107 | // 检测 汇总信息
108 | it('check sum', (done) => {
109 | const aFootRow = vm.$el.querySelectorAll('.flex-table-foot .flex-table-row .flex-table-col');
110 | const aFootLabel: string[] = [];
111 | const aFootValue: string[] = [];
112 | aFootRow.forEach((node) => {
113 | const aDoms = node.children;
114 | setFoot(aDoms, aFootValue, aFootLabel);
115 | });
116 | expect(aFootValue).to.eql(['Jim Green', 'age: 24', 'London', '2016-10-01']);
117 | expect(aFootLabel).to.eql(['Name', 'Age', 'Address', 'Date']);
118 | done();
119 | });
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/test/unit/specs/resizable.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createVue,
3 | destroyVM,
4 | } from '@/util';
5 | import Vue from 'vue';
6 | import { expect } from 'chai';
7 |
8 | const aTestList: FlexTableColumnOption[] = [];
9 | for (let i = 0; i < 5; i += 1) {
10 | const oTestData = {
11 | name: 'John Brown',
12 | age: 18,
13 | address: 'New York No. 1 Lake Park',
14 | date: '2016-10-03',
15 | };
16 | aTestList.push(oTestData);
17 | }
18 |
19 |
20 | describe('Flex-Table', () => {
21 | // 基础测试
22 | describe('resizable', () => {
23 | const nInitWidth = 50;
24 | const nAddWidth = 100;
25 | const vm: Vue = createVue({
26 | template: `
27 |
34 | `,
35 | data() {
36 | return {
37 | columns: [
38 | {
39 | title: 'Name',
40 | key: 'name',
41 | width: nInitWidth,
42 | renderHeader(h: Vue.CreateElement, params: FlexTableRow) {
43 | return h('span', `Custom Title : ${params.column.title}`);
44 | },
45 | },
46 | {
47 | title: 'Age',
48 | key: 'age',
49 | width: nInitWidth,
50 | resizable: false,
51 | render(h: Vue.CreateElement, params: FlexTableRow) {
52 | return h('span', `age: ${params.row.age}`);
53 | },
54 | },
55 | {
56 | title: 'Address',
57 | key: 'address',
58 | },
59 | {
60 | title: 'Date',
61 | key: 'date',
62 | },
63 | ],
64 | loading: false,
65 | list: aTestList,
66 | sum: {
67 | name: 'Jim Green',
68 | age: 24,
69 | address: 'London',
70 | date: '2016-10-01',
71 | },
72 | };
73 | },
74 | });
75 |
76 | const $resizeDiv = vm.$el.querySelectorAll('.flex-table-head .flex-table-col-resize')[0];
77 | const $resizeDivAge = vm.$el.querySelectorAll('.flex-table-head .flex-table-col-resize')[1];
78 | const vmTable: any = vm.$children[0];
79 | const vmHeaer = vmTable.$children[0];
80 |
81 | vmHeaer.onColResize.call(vmHeaer, {
82 | clientX: 0,
83 | target: $resizeDiv,
84 | stopPropagation: () => void 0,
85 | }, 0);
86 |
87 | vmTable.onColResizeMove.call(vmTable, {
88 | clientX: nAddWidth,
89 | target: $resizeDiv,
90 | stopPropagation: () => void 0,
91 | });
92 |
93 | vmHeaer.onColResize.call(vmHeaer, {
94 | clientX: 0,
95 | target: $resizeDivAge,
96 | stopPropagation: () => void 0,
97 | }, 0);
98 |
99 | vmTable.onColResizeMove.call(vmTable, {
100 | clientX: nAddWidth,
101 | target: $resizeDivAge,
102 | stopPropagation: () => void 0,
103 | });
104 |
105 | vmTable.onColResizeEnd.call(vmTable);
106 |
107 | // 检测
108 | it('可以调整宽度', () => {
109 | const row = vmTable.tableColumns[0];
110 | expect(row.width).to.eql(nInitWidth + nAddWidth);
111 | });
112 |
113 | it('不可调整宽度', () => {
114 | const row = vmTable.tableColumns[1];
115 | expect(row.width).to.eql(nInitWidth);
116 | });
117 |
118 | destroyVM(vm);
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/test/unit/specs/scopedSlot.spec.ts:
--------------------------------------------------------------------------------
1 | import { createVue } from '@/util';
2 | import { expect } from 'chai';
3 | import Vue from 'vue';
4 |
5 | const aTestList: FlexTableColumnOption[] = [];
6 | const aTestBtn: string[] = [];
7 | const aTestHtml: string[] = [];
8 | for (let i = 0; i < 5; i += 1) {
9 | const sCon = `2016-10-03(${i})`;
10 | const oTestData = {
11 | name: 'John Brown',
12 | age: 18 + i,
13 | address: 'New York No. 1 Lake Park',
14 | date: `${sCon}`,
15 | };
16 | aTestList.push(oTestData);
17 | aTestBtn.push(`View${i}`);
18 | aTestHtml.push(sCon);
19 | }
20 |
21 | describe('Flex-Table', () => {
22 | // 基础测试
23 | describe('scopedSlot', () => {
24 | const vm: Vue = createVue({
25 | template: `
26 |
31 |
32 |
33 |
34 |
35 | `,
36 | data() {
37 | return {
38 | columns: [
39 | {
40 | title: 'Name',
41 | key: 'name',
42 | },
43 | {
44 | title: 'Age',
45 | key: 'age',
46 | },
47 | {
48 | title: 'Address',
49 | key: 'address',
50 | },
51 | {
52 | title: 'Date',
53 | key: 'date',
54 | type: 'html',
55 | },
56 | {
57 | title: 'operation',
58 | key: 'operation',
59 | type: 'slot',
60 | },
61 | ],
62 | loading: false,
63 | list: aTestList,
64 | };
65 | },
66 | methods: {
67 | },
68 | });
69 |
70 | // 检测 slot
71 | it('check slot', async () => {
72 | const aOperation = vm.$el.querySelectorAll('.flex-table-body button');
73 | const aBtnStr: string[] = [];
74 | aOperation.forEach((elem) => {
75 | if (elem && elem.textContent) {
76 | aBtnStr.push(elem.textContent);
77 | }
78 | });
79 |
80 | expect(aTestBtn).to.eql(aBtnStr);
81 | });
82 |
83 | it('check html', async () => {
84 | const aOperation = vm.$el.querySelectorAll('.flex-table-body i');
85 | const aHtmlStr: string[] = [];
86 | aOperation.forEach((elem) => {
87 | if (elem && elem.textContent) {
88 | aHtmlStr.push(elem.textContent);
89 | }
90 | });
91 |
92 | expect(aTestHtml).to.eql(aHtmlStr);
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/test/unit/specs/scrollBar.spec.ts:
--------------------------------------------------------------------------------
1 | import { createVue, triggerEvent, wait } from '@/util';
2 | import { expect } from 'chai';
3 | import Vue from 'vue';
4 |
5 | const aTestList: FlexTableColumnOption[] = [];
6 | for (let i = 0; i < 5; i += 1) {
7 | const sCon = `2016-10-03(${i})`;
8 | const oTestData = {
9 | name: 'John Brown',
10 | age: 18 + i,
11 | address: 'New York No. 1 Lake Park',
12 | date: `${sCon}`,
13 | };
14 | aTestList.push(oTestData);
15 | }
16 |
17 | describe('Flex-Table', () => {
18 | // 基础测试
19 | describe('scrollBar', () => {
20 | const vm: Vue = createVue({
21 | template: `
22 |
27 |
28 | `,
29 | data() {
30 | return {
31 | columns: [
32 | {
33 | title: 'Name',
34 | key: 'name',
35 | },
36 | {
37 | title: 'Age',
38 | key: 'age',
39 | },
40 | {
41 | title: 'Address',
42 | key: 'address',
43 | },
44 | {
45 | title: 'Date',
46 | key: 'date',
47 | },
48 | {
49 | title: 'operation',
50 | key: 'operation',
51 | },
52 | ],
53 | loading: false,
54 | height: 200,
55 | list: aTestList,
56 | };
57 | },
58 | methods: {
59 | },
60 | });
61 |
62 | // 检测 滚动条
63 | it('check', async () => {
64 | const vmTable: any = vm.$children[0];
65 | vmTable.bodyH = 210; // 不能获取offsetHeight,所以这样处理
66 | await wait(0);
67 | const srcollY = vm.$el.querySelectorAll('.flex-table-scroll-y');
68 | expect(srcollY.length).to.eql(1);
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/unit/specs/selectable.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createVue,
3 | triggerEvent,
4 | wait,
5 | } from '@/util';
6 | import { expect } from 'chai';
7 | import Vue from 'vue';
8 |
9 | const aTestList: FlexTableColumnOption[] = [];
10 | for (let i = 0; i < 2; i += 1) {
11 | const oTestData = {
12 | name: 'John Brown',
13 | age: 18,
14 | address: 'New York No. 1 Lake Park',
15 | date: '2016-10-03',
16 | };
17 | aTestList.push(oTestData);
18 | }
19 |
20 |
21 | describe('Flex-Table', () => {
22 | // 基础测试
23 | describe('select', () => {
24 | const vm: any = createVue({
25 | template: `
26 |
33 | `,
34 | data() {
35 | return {
36 | columns: [
37 | {
38 | type: 'selection',
39 | width: 20,
40 | align: 'center',
41 | },
42 | {
43 | title: 'Name',
44 | key: 'name',
45 | },
46 | {
47 | title: 'Age',
48 | key: 'age',
49 | render(h: Vue.CreateElement, params: FlexTableRow) {
50 | return h('span', `age: ${params.row.age}`);
51 | },
52 | },
53 | {
54 | title: 'Address',
55 | key: 'address',
56 | },
57 | {
58 | title: 'Date',
59 | key: 'date',
60 | },
61 | ],
62 | loading: false,
63 | list: aTestList,
64 | sum: {
65 | name: 'Jim Green',
66 | age: 24,
67 | address: 'London',
68 | date: '2016-10-01',
69 | },
70 | };
71 | },
72 | });
73 | const elemAllCheckedBtn: any = vm.$el.querySelector('.flex-table-head input[type="checkbox"]');
74 | // 检测 全选
75 | it('check select all', async () => {
76 | triggerEvent(elemAllCheckedBtn, 'click');
77 | vm.$children[0].$children[0].$children[0].toggle(); // 这里需要手动程序触发
78 | await wait(100);
79 | let bCheck = true;
80 | const aElemBodyCheck = vm.$el.querySelectorAll('.flex-table-body input[type="checkbox"]');
81 |
82 | aElemBodyCheck.forEach((element: any) => {
83 | if (!element.checked) {
84 | bCheck = false;
85 | }
86 | });
87 |
88 | expect(bCheck).to.eql(true);
89 | });
90 |
91 | // 检测取消全选
92 | it('check unselect all', async () => {
93 | triggerEvent(elemAllCheckedBtn, 'click');
94 | await wait(100);
95 | vm.$children[0].$children[0].$children[0].toggle(); // 这里需要手动程序触发
96 | await wait(100);
97 | let bCheck = true;
98 | const aElemBodyCheck = vm.$el.querySelectorAll('.flex-table-body input[type="checkbox"]');
99 |
100 | aElemBodyCheck.forEach((element: any) => {
101 | if (element.checked) {
102 | bCheck = false;
103 | }
104 | });
105 |
106 | expect(bCheck).to.eql(true);
107 | });
108 |
109 | // 检测 全选,有diabled的情况
110 | it('check select all-_isDisabled', async () => {
111 | vm.$children[0].dataList[0]._isDisabled = true;
112 | triggerEvent(elemAllCheckedBtn, 'click');
113 | vm.$children[0].$children[0].$children[0].toggle(); // 这里需要手动程序触发
114 | await wait(100);
115 | const aCheck: boolean[] = [];
116 | const aElemBodyCheck = vm.$el.querySelectorAll('.flex-table-body input[type="checkbox"]');
117 |
118 | aElemBodyCheck.forEach((element: any) => {
119 | aCheck.push(element.checked);
120 | });
121 |
122 | expect(aCheck).to.eql([false, true]);
123 | });
124 |
125 | // 检测 取消全选,有diabled的情况
126 | it('check unselect all-_isDisabled', async () => {
127 | vm.$children[0].dataList[0]._isDisabled = true;
128 | triggerEvent(elemAllCheckedBtn, 'click');
129 | vm.$children[0].$children[0].$children[0].toggle(); // 这里需要手动程序触发
130 | await wait(100);
131 | const aCheck: boolean[] = [];
132 | const aElemBodyCheck = vm.$el.querySelectorAll('.flex-table-body input[type="checkbox"]');
133 |
134 | aElemBodyCheck.forEach((element: any) => {
135 | aCheck.push(element.checked);
136 | });
137 |
138 | expect(aCheck).to.eql([false, false]);
139 | });
140 |
141 | // 检测 全选后,点击body中一个input 此时全选应该被取消
142 | it('check unselect all->body unselect', async () => {
143 | vm.$children[0].dataList[0]._isDisabled = false;
144 | triggerEvent(elemAllCheckedBtn, 'click');
145 | vm.$children[0].$children[0].$children[0].toggle(); // 这里需要手动程序触发
146 | await wait(100);
147 | const aElemBodyCheck = vm.$el.querySelectorAll('.flex-table-body input[type="checkbox"]');
148 | triggerEvent(aElemBodyCheck[0], 'click');
149 | vm.$children[0].$children[1].$children[0].toggleSelect(0); // 这里需要手动程序触发
150 | await wait(100);
151 | const bCheck = elemAllCheckedBtn.checked;
152 |
153 | expect(bCheck).to.eql(false);
154 |
155 | // 还原回去
156 | triggerEvent(aElemBodyCheck[0], 'click');
157 | vm.$children[0].$children[1].$children[0].toggleSelect(0); // 这里需要手动程序触发
158 | });
159 |
160 | // 检测 点击body中的input 此时全选应该被选中
161 | it('check select all->body select', async () => {
162 | vm.$children[0].dataList[0]._isDisabled = false;
163 | const aElemBodyCheck = vm.$el.querySelectorAll('.flex-table-body input[type="checkbox"]');
164 | aElemBodyCheck.forEach(async (elem: Element, index: number) => {
165 | triggerEvent(elem, 'click');
166 | vm.$children[0].$children[1].$children[0].toggleSelect(index); // 这里需要手动程序触发
167 | await wait(50);
168 | });
169 | const bCheck = elemAllCheckedBtn && elemAllCheckedBtn.checked;
170 |
171 | expect(bCheck).to.eql(true);
172 | });
173 |
174 | // 检测 没有数据时 点击全选
175 | it('check select all->no data', async () => {
176 | vm.list = [];
177 | await wait(100);
178 | triggerEvent(elemAllCheckedBtn, 'click');
179 | await wait(100);
180 | const bCheck = vm.$children[0].$children[0].$children[0].state; // 属性有问题,这里改成用组件的state判断
181 | expect(bCheck).to.eql(false);
182 | });
183 | });
184 | });
185 |
--------------------------------------------------------------------------------
/test/unit/specs/sortable.spec.ts:
--------------------------------------------------------------------------------
1 | import { createVue, triggerEvent, waitImmediate } from '@/util';
2 | import { expect } from 'chai';
3 | import Vue from 'vue';
4 |
5 | const aTestList: FlexTableColumnOption[] = [];
6 | for (let i = 0; i < 5; i += 1) {
7 | const oTestData = {
8 | name: 'John Brown',
9 | age: 18 + i,
10 | address: 'New York No. 1 Lake Park',
11 | date: '2016-10-03',
12 | };
13 | aTestList.push(oTestData);
14 | }
15 |
16 | describe('Flex-Table', () => {
17 | // 基础测试
18 | describe('sortable', () => {
19 | const vm: Vue = createVue({
20 | template: `
21 |
29 | `,
30 | data() {
31 | return {
32 | columns: [
33 | {
34 | title: 'Name',
35 | key: 'name',
36 | },
37 | {
38 | title: 'Age',
39 | key: 'age',
40 | sortable: true,
41 | },
42 | {
43 | title: 'Address',
44 | key: 'address',
45 | },
46 | {
47 | title: 'Date',
48 | key: 'date',
49 | },
50 | ],
51 | loading: false,
52 | list: aTestList,
53 | sum: {
54 | name: 'Jim Green',
55 | age: 24,
56 | address: 'London',
57 | date: '2016-10-01',
58 | },
59 | };
60 | },
61 | methods: {
62 | onSortChange(obj: SortOption) {
63 | const list = vm.$data.list;
64 | const sKey = obj.key;
65 | const sOrder = obj.order;
66 | const aList = list.sort((item1: any, item2: any) => {
67 | if (sOrder === 'desc') {
68 | return item2[sKey] - item1[sKey];
69 | }
70 | return item1[sKey] - item2[sKey];
71 | });
72 | vm.$data.list = aList;
73 | },
74 | },
75 | });
76 |
77 | const sSortSelector = '.flex-table-head .flex-table-col:nth-child(2) .flex-table-sort i';
78 | const aHeadAgeSort = vm.$el.querySelectorAll(sSortSelector);
79 |
80 | // 检测 倒叙
81 | it('check desc', async () => {
82 | triggerEvent(aHeadAgeSort[1], 'click');
83 | await checkOrder(vm, 'desc');
84 | });
85 |
86 | // 检测 升序
87 | it('check asc', async () => {
88 | triggerEvent(aHeadAgeSort[0], 'click');
89 | await checkOrder(vm, 'asc');
90 | });
91 |
92 | // 检测 动态修改columns
93 | it('change columns: desc', async () => {
94 | await chnageColumns(vm, 'desc');
95 | });
96 |
97 | it('change columns: asc', async () => {
98 | await chnageColumns(vm, 'asc');
99 | });
100 | });
101 | });
102 | async function chnageColumns(vm: Vue, type: string) {
103 | vm.$data.columns = [
104 | {
105 | title: 'Name',
106 | key: 'name',
107 | },
108 | {
109 | title: 'Age',
110 | key: 'age',
111 | },
112 | {
113 | title: 'Address',
114 | key: 'address',
115 | },
116 | {
117 | title: 'Date',
118 | key: 'date',
119 | sortable: true,
120 | sortType: type,
121 | },
122 | ];
123 | await waitImmediate();
124 | let sOrderSelector = '.flex-table-head .flex-table-col:nth-child(4) .flex-table-sort .flex-table-arrow-';
125 | sOrderSelector += (type === 'desc' ? 'dropdown' : 'dropup');
126 | const elemHeadDateSort = vm.$el.querySelector(sOrderSelector);
127 | let bCheck = false;
128 | if (elemHeadDateSort && elemHeadDateSort.classList) {
129 | bCheck = elemHeadDateSort.classList.contains('on');
130 | }
131 | expect(bCheck).to.eql(true);
132 | }
133 |
134 | async function checkOrder(vm: Vue, type: string) {
135 | let aOrderList: FlexTableColumnOption[] = JSON.parse(JSON.stringify(aTestList));
136 | aOrderList = aOrderList.sort((item1: any, item2: any) => {
137 | if ( type === 'desc') {
138 | return item2.age - item1.age;
139 | }
140 | return item1.age - item2.age;
141 | });
142 | const aTestData: string[] = [];
143 | aOrderList.forEach((item) => {
144 | Object.keys(item).forEach((k) => {
145 | aTestData.push(item[k].toString());
146 | });
147 | });
148 | await waitImmediate();
149 | const aBodyRow: NodeListOf = vm.$el.querySelectorAll('.flex-table-body .flex-table-row');
150 | const aBodyData: string[] = [];
151 | aBodyRow.forEach((node) => {
152 | const aCol = node.querySelectorAll('.flex-table-col');
153 | aCol.forEach((elem) => {
154 | if (elem && elem.textContent) {
155 | aBodyData.push(elem.textContent.trim());
156 | }
157 | });
158 | });
159 | expect(aBodyData).to.eql(aTestData);
160 | }
161 |
162 |
--------------------------------------------------------------------------------
/test/unit/specs/theme.spec.ts:
--------------------------------------------------------------------------------
1 | import { createVue, waitImmediate } from '@/util';
2 | import { expect } from 'chai';
3 | import Vue from 'vue';
4 |
5 | const aTestList: FlexTableColumnOption[] = [];
6 | for (let i = 0; i < 5; i += 1) {
7 | const oTestData = {
8 | name: 'John Brown',
9 | age: 18 + i,
10 | address: 'New York No. 1 Lake Park',
11 | date: '2016-10-03',
12 | };
13 | aTestList.push(oTestData);
14 | }
15 |
16 | describe('Flex-Table', () => {
17 | // 基础测试
18 | describe('theme', () => {
19 | const vm: Vue = createVue({
20 | template: `
21 |
30 | `,
31 | data() {
32 | return {
33 | columns: [
34 | {
35 | title: 'Name',
36 | key: 'name',
37 | },
38 | {
39 | title: 'Age',
40 | key: 'age',
41 | sortable: true,
42 | },
43 | {
44 | title: 'Address',
45 | key: 'address',
46 | },
47 | {
48 | title: 'Date',
49 | key: 'date',
50 | },
51 | ],
52 | loading: false,
53 | list: aTestList,
54 | sum: {
55 | name: 'Jim Green',
56 | age: 24,
57 | address: 'London',
58 | date: '2016-10-01',
59 | },
60 | size: '',
61 | theme: '',
62 | };
63 | },
64 | methods: {
65 | },
66 | });
67 |
68 | // 检测 size-big
69 | it('check size-big', async () => {
70 | vm.$data.size = 'big';
71 |
72 | await waitImmediate();
73 | const bBig = vm.$el.classList.contains('flex-table-big');
74 | expect(bBig).to.eql(true);
75 | });
76 |
77 | // 检测 size-small
78 | it('check size-small', async () => {
79 | vm.$data.size = 'small';
80 |
81 | await waitImmediate();
82 | const bBig = vm.$el.classList.contains('flex-table-small');
83 | expect(bBig).to.eql(true);
84 | });
85 |
86 | // 检测 theme-dark
87 | it('check theme-dark', async () => {
88 | vm.$data.theme = 'dark';
89 |
90 | await waitImmediate();
91 | const bBig = vm.$el.classList.contains('flex-table-dark');
92 | expect(bBig).to.eql(true);
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/test/unit/tool.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * set foot
3 | * @param aDoms
4 | * @param aFootValue
5 | * @param aFootLabel
6 | */
7 | // tslint:disable-next-line:max-line-length
8 | export function setFoot(aDoms: NodeListOf | HTMLCollection, aFootValue: string[], aFootLabel: string[]) {
9 | if (aDoms[0] && aDoms[0].textContent) {
10 | aFootValue.push(aDoms[0].textContent);
11 | }
12 | if (aDoms[1] && aDoms[1].textContent) {
13 | aFootLabel.push(aDoms[1].textContent);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/unit/util.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import flexTable from '../../index';
3 |
4 | Vue.use(flexTable);
5 |
6 | let id = 0;
7 |
8 | const createElm = function() {
9 | const elm = document.createElement('div');
10 |
11 | elm.id = 'app' + ++id;
12 | document.body.appendChild(elm);
13 |
14 | return elm;
15 | };
16 |
17 | /**
18 | * 回收 vm
19 | * @param {Object} vm
20 | */
21 | export const destroyVM = function(vm: Vue) {
22 | if (vm.$destroy) {
23 | vm.$destroy();
24 | }
25 | if (vm.$el && vm.$el.parentNode) {
26 | vm.$el.parentNode.removeChild(vm.$el);
27 | }
28 | };
29 |
30 | /**
31 | * 创建一个 Vue 的实例对象
32 | * @param {Object|String} Compo 组件配置,可直接传 template
33 | * @param {Boolean=false} mounted 是否添加到 DOM 上
34 | * @return {Object} vm
35 | */
36 | export const createVue = function(Compo: any, mounted = false) {
37 | if (Object.prototype.toString.call(Compo) === '[object String]') {
38 | Compo = { template: Compo };
39 | }
40 | return new Vue(Compo).$mount(mounted === false ? undefined : createElm());
41 | };
42 |
43 | /**
44 | * 创建一个测试组件实例
45 | * @link http://vuejs.org/guide/unit-testing.html#Writing-Testable-Components
46 | * @param {Object} Compo - 组件对象
47 | * @param {Object} propsData - props 数据
48 | * @param {Boolean=false} mounted - 是否添加到 DOM 上
49 | * @return {Object} vm
50 | */
51 | export const createTest = function(Compo: any, propsData = {}, mounted = false) {
52 | if (propsData === true || propsData === false) {
53 | mounted = !!propsData;
54 | propsData = {};
55 | }
56 | const elm = createElm();
57 | const Ctor = Vue.extend(Compo);
58 | return new Ctor({ propsData }).$mount(mounted === false ? undefined : elm);
59 | };
60 |
61 | /**
62 | * 触发一个事件
63 | * mouseenter, mouseleave, mouseover, keyup, change, click 等
64 | * @param {Element} elm
65 | * @param {String} name
66 | * @param {*} opts
67 | */
68 | export const triggerEvent = function(elm: any, name: string, ...opts: any[]) {
69 | let eventName;
70 |
71 | if (/^mouse|click/.test(name)) {
72 | eventName = 'MouseEvents';
73 | } else if (/^key/.test(name)) {
74 | eventName = 'KeyboardEvent';
75 | } else {
76 | eventName = 'HTMLEvents';
77 | }
78 | const evt = document.createEvent(eventName);
79 |
80 | evt.initEvent(name, ...opts);
81 | elm.dispatchEvent
82 | ? elm.dispatchEvent(evt)
83 | : elm.fireEvent('on' + name, evt);
84 |
85 | return elm;
86 | };
87 |
88 | /**
89 | * 触发 “mouseup” 和 “mousedown” 事件
90 | * @param {Element} elm
91 | * @param {*} opts
92 | */
93 | export const triggerClick = function(elm: HTMLElement, ...opts: any[]) {
94 | triggerEvent(elm, 'mousedown', ...opts);
95 | triggerEvent(elm, 'mouseup', ...opts);
96 |
97 | return elm;
98 | };
99 |
100 | /**
101 | * 触发 keydown 事件
102 | * @param {Element} elm
103 | * @param {keyCode} int
104 | */
105 | export const triggerKeyDown = function(el: HTMLElement, keyCode: number|string) {
106 | const evt: any = document.createEvent('Events');
107 | evt.initEvent('keydown', true, true);
108 | evt.keyCode = keyCode;
109 | el.dispatchEvent(evt);
110 | };
111 |
112 | /**
113 | * 等待 ms 毫秒,返回 Promise
114 | * @param {Number} ms
115 | */
116 | export const wait = function(ms: number = 50) {
117 | return new Promise((resolve) => setTimeout(() => resolve(), ms));
118 | };
119 |
120 | /**
121 | * 等待一个 Tick,代替 Vue.nextTick,返回 Promise
122 | */
123 | export const waitImmediate = () => wait(0);
124 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode, CreateElement } from "vue";
2 |
3 | interface sortOption {
4 | key: string;
5 | order: string;
6 | }
7 |
8 | export declare class FlexTable extends Vue {
9 | /**
10 | * 显示数据
11 | */
12 | list: object[];
13 |
14 | /**
15 | * 汇总信息
16 | */
17 | sum?: object | boolean;
18 |
19 | /**
20 | * 表格列的配置
21 | */
22 | columns: object[];
23 |
24 | /**
25 | * 加载状态, default: false
26 | */
27 | loading: boolean;
28 |
29 | /**
30 | * 高度
31 | */
32 | height?: number;
33 |
34 | /**
35 | * 是否可以调整列宽,default: false
36 | */
37 | resizable?: boolean;
38 |
39 | /**
40 | * 没有数据时显示的文案, default: 'No Data'
41 | */
42 | noData?: string;
43 |
44 | /**
45 | * 排序发生变化时触发
46 | * @returns {Object} option
47 | *
48 | */
49 | $emit(
50 | eventName: "on-sort-change",
51 | option: sortOption
52 | ): this;
53 |
54 | /**
55 | * 拖拽调整列宽时触发
56 | * @returns newWidth, oldWidth, column, event
57 | *
58 | */
59 | $emit(
60 | eventName: "on-col-width-resize",
61 | newWidth: number,
62 | oldWidth: number,
63 | column: object,
64 | event: MouseEvent
65 | ): this;
66 | }
67 |
--------------------------------------------------------------------------------