├── .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 | 25 | 35 | 113 | -------------------------------------------------------------------------------- /examples/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | iview example 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 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 | 8 | 9 | 78 | 79 | 96 | -------------------------------------------------------------------------------- /examples/features/autoCalWidth.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /examples/features/expand.vue: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /examples/features/expandRow.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/features/fixedHeader.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /examples/features/fixedLeft.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /examples/features/fixedLeft1.vue: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /examples/features/fixedLeft2.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /examples/features/footer.vue: -------------------------------------------------------------------------------- 1 | 17 | 72 | -------------------------------------------------------------------------------- /examples/features/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 189 | -------------------------------------------------------------------------------- /examples/features/loading.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /examples/features/render.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /examples/features/resizable.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /examples/features/scopedSlot.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /examples/features/selectable.vue: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /examples/features/sortable.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /examples/features/span.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /examples/features/style.vue: -------------------------------------------------------------------------------- 1 | 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 | ![flextable](https://user-images.githubusercontent.com/6723674/57348072-4db8ef80-7187-11e9-98eb-2b613073f266.gif) 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 | ![image](https://github.com/stzhongjie/FlexTable/blob/master/src/img/demo.gif) 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 | 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 | 37 | 110 | -------------------------------------------------------------------------------- /src/tableLoadingBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 105 | 133 | -------------------------------------------------------------------------------- /src/tableScrollBar.vue: -------------------------------------------------------------------------------- 1 | 15 | 51 | -------------------------------------------------------------------------------- /src/tableSum.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 159 | 187 | -------------------------------------------------------------------------------- /src/tableTr.vue: -------------------------------------------------------------------------------- 1 | 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 | 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('

John Brown

'); 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 | 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 | --------------------------------------------------------------------------------