├── static └── .gitkeep ├── .eslintignore ├── WechatIMG132.jpeg ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── src ├── grid │ ├── error.png │ ├── more.png │ ├── offcheck.png │ ├── oncheck.png │ ├── history.js │ ├── checkbox.vue │ ├── scroller.js │ ├── painted.js │ ├── index.vue │ ├── events.js │ └── calculate.js ├── App.vue ├── router │ └── index.js ├── main.js └── components │ └── Example.vue ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── index.html ├── .babelrc ├── README.md ├── .eslintrc.js └── package.json /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /WechatIMG132.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyvirus/vue-grid-canvas/HEAD/WechatIMG132.jpeg -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /src/grid/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyvirus/vue-grid-canvas/HEAD/src/grid/error.png -------------------------------------------------------------------------------- /src/grid/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyvirus/vue-grid-canvas/HEAD/src/grid/more.png -------------------------------------------------------------------------------- /src/grid/offcheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyvirus/vue-grid-canvas/HEAD/src/grid/offcheck.png -------------------------------------------------------------------------------- /src/grid/oncheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubyvirus/vue-grid-canvas/HEAD/src/grid/oncheck.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-grid-canvas 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Example from '@/components/Example' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'Example', 12 | component: Example, 13 | }, 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | 7 | Vue.config.productionTip = false 8 | 9 | /* eslint-disable no-new */ 10 | new Vue({ 11 | el: '#app', 12 | router, 13 | template: '', 14 | components: { App }, 15 | }) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-grid-canvas 2 | 3 | > a vue component,基于vue的表格组件,主要解决大数据量的表格渲染性能问题,使用canvas绘制表格,同时支持类似excel的批量选中,复制黏贴删除,实时编辑等功能。 4 | 5 | ![vue-grid-canvas](https://github.com/Harveyzhao/vue-grid-canvas/blob/master/WechatIMG132.jpeg?raw=true) 6 | 7 | ### 目前测试阶段,暂不发布npm仓库,定制性较弱,需要日后完善,有需要的朋友,可参考example使用 8 | 9 | 一个类似excel的表格组件,说明: 10 | * 1,通过canvas实现,能处理万级数据 11 | * 2,类似excel,选中单元格并实时编辑 12 | * 3,复制黏贴,支持批量,从excel复制,复制到excel都可以 13 | * 4,撤销/前进 14 | * 5,checkbox勾选框,全选功能,可开关 15 | * 6,固定列(目前只支持固定到右侧) 16 | * 7,删除单元格,支持批量 17 | * 7,支持文本的重新计算渲染(通过计算的单元格不支持实时编辑) 18 | * 8,支持基础按钮显示及点击事件 19 | * 9,隐藏列功能,可开关 20 | 21 | 22 | 以后计划: 23 | * 1,由于使用canvas不支持浏览器的检索功能,以后加上表格的搜索功能 24 | * 2,行列拖拽 25 | 26 | 27 | 28 | ## 运行例子 29 | 30 | ``` bash 31 | # install dependencies 32 | npm install 33 | 34 | # serve with hot reload at localhost:8080 35 | npm run dev 36 | 37 | # build for production with minification 38 | npm run build 39 | 40 | # build for production and view the bundle analyzer report 41 | npm run build --report 42 | ``` 43 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | extends: 'airbnb-base', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': 'build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | // add your custom rules here 26 | 'rules': { 27 | // don't require .vue extension when importing 28 | 'import/extensions': ['error', 'always', { 29 | 'js': 'never', 30 | 'vue': 'never' 31 | }], 32 | // allow optionalDependencies 33 | 'import/no-extraneous-dependencies': ['error', { 34 | 'optionalDependencies': ['test/unit/index.js'] 35 | }], 36 | // allow debugger during development 37 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 38 | "indent": [2, 4], 39 | "eol-last": 1, 40 | "semi": [2, "never"], 41 | "no-restricted-syntax": 0, 42 | "no-param-reassign": 0, 43 | "max-len": 0, 44 | "no-labels": 0, 45 | "no-continue": 0, 46 | "global-require": 0, 47 | "import/no-dynamic-require": 0, 48 | "no-shadow": 0, 49 | "no-underscore-dangle": 0, 50 | "linebreak-style": 0, 51 | "linebreak-style": 0 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.1 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../dist/index.html'), 12 | assetsRoot: path.resolve(__dirname, '../dist'), 13 | assetsSubDirectory: 'static', 14 | assetsPublicPath: '/', 15 | productionSourceMap: true, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: process.env.PORT || 8080, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: {}, 35 | // CSS Sourcemaps off by default because relative paths are "buggy" 36 | // with this option, according to the CSS-Loader README 37 | // (https://github.com/webpack/css-loader#sourcemaps) 38 | // In our experience, they generally work as expected, 39 | // just be aware of this issue when enabling this option. 40 | cssSourceMap: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-grid-canvas", 3 | "version": "1.0.0", 4 | "description": "a vue component", 5 | "author": "Harvey Zhao ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js", 11 | "lint": "eslint --ext .js,.vue src" 12 | }, 13 | "dependencies": { 14 | "throttle-debounce": "^1.0.1", 15 | "vue": "^2.4.2", 16 | "vue-router": "^2.7.0" 17 | }, 18 | "devDependencies": { 19 | "autoprefixer": "^7.1.2", 20 | "babel-core": "^6.22.1", 21 | "babel-eslint": "^7.1.1", 22 | "babel-loader": "^7.1.1", 23 | "babel-plugin-transform-runtime": "^6.22.0", 24 | "babel-preset-env": "^1.3.2", 25 | "babel-preset-stage-2": "^6.22.0", 26 | "babel-register": "^6.22.0", 27 | "chalk": "^2.0.1", 28 | "connect-history-api-fallback": "^1.3.0", 29 | "copy-webpack-plugin": "^4.0.1", 30 | "css-loader": "^0.28.0", 31 | "eslint": "^3.19.0", 32 | "eslint-config-airbnb-base": "^11.3.0", 33 | "eslint-friendly-formatter": "^3.0.0", 34 | "eslint-import-resolver-webpack": "^0.8.3", 35 | "eslint-loader": "^1.7.1", 36 | "eslint-plugin-html": "^3.0.0", 37 | "eslint-plugin-import": "^2.7.0", 38 | "eventsource-polyfill": "^0.9.6", 39 | "express": "^4.14.1", 40 | "extract-text-webpack-plugin": "^3.0.0", 41 | "file-loader": "^1.1.4", 42 | "friendly-errors-webpack-plugin": "^1.6.1", 43 | "html-webpack-plugin": "^2.30.1", 44 | "http-proxy-middleware": "^0.17.3", 45 | "node-sass": "^4.5.3", 46 | "opn": "^5.1.0", 47 | "optimize-css-assets-webpack-plugin": "^3.2.0", 48 | "ora": "^1.2.0", 49 | "portfinder": "^1.0.13", 50 | "rimraf": "^2.6.0", 51 | "sass-loader": "^6.0.6", 52 | "semver": "^5.3.0", 53 | "shelljs": "^0.7.6", 54 | "url-loader": "^0.5.8", 55 | "vue-loader": "^13.0.4", 56 | "vue-style-loader": "^3.0.1", 57 | "vue-template-compiler": "^2.4.2", 58 | "webpack": "^3.6.0", 59 | "webpack-bundle-analyzer": "^2.9.0", 60 | "webpack-dev-middleware": "^1.12.0", 61 | "webpack-hot-middleware": "^2.18.2", 62 | "webpack-merge": "^4.1.0" 63 | }, 64 | "engines": { 65 | "node": ">= 4.0.0", 66 | "npm": ">= 3.0.0" 67 | }, 68 | "browserslist": [ 69 | "> 1%", 70 | "last 2 versions", 71 | "not ie <= 8" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /src/grid/history.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | return { 4 | history: [], 5 | historyIndex: 0, 6 | currentStateValue: null, 7 | } 8 | }, 9 | created() { 10 | this.$on('history_back', () => { 11 | if (this.historyIndex >= 0) { 12 | const needBackValue = this.history[this.historyIndex] 13 | if (needBackValue.type === 'edit') { 14 | this.saveItem(needBackValue.before, false) 15 | this.focusCellByOriginCell(this.getCellByRowAndKey(needBackValue.before.index, needBackValue.before.key)) 16 | this.selectArea = null 17 | this.isSelect = false 18 | } else if (needBackValue.type === 'editMore') { 19 | this.$emit('updateValue', this.saveItems(needBackValue.before, false)) 20 | this.focusCell = needBackValue.focusCell 21 | this.rowFocus = { 22 | cellX: this.focusCell.x, 23 | cellY: this.focusCell.y, 24 | rowIndex: this.focusCell.rowIndex, 25 | offset: { ...this.offset }, 26 | } 27 | this.paintFocusCell(this.focusCell) 28 | this.selectArea = needBackValue.selectArea 29 | this.isSelect = true 30 | this.rePainted() 31 | } 32 | this.historyIndex -= 1 33 | } 34 | }) 35 | this.$on('history_forward', () => { 36 | if (this.historyIndex !== this.history.length - 1) { 37 | this.historyIndex += 1 38 | const needforwardValue = this.history[this.historyIndex] 39 | if (needforwardValue.type === 'edit') { 40 | this.saveItem(needforwardValue.after, false) 41 | this.focusCellByOriginCell(this.getCellByRowAndKey(needforwardValue.after.index, needforwardValue.after.key)) 42 | } 43 | } 44 | }) 45 | }, 46 | methods: { 47 | pushState(data) { 48 | this.history = this.history.slice(0, this.historyIndex + 1) 49 | this.history.push(data) 50 | if (this.history.length > 20) { 51 | this.history.splice(0, 1) 52 | } 53 | this.historyIndex = this.history.length - 1 54 | }, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Example.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 72 | -------------------------------------------------------------------------------- /src/grid/checkbox.vue: -------------------------------------------------------------------------------- 1 | 10 | 94 | 95 | 187 | -------------------------------------------------------------------------------- /src/grid/scroller.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | return { 4 | horizontalBar: { 5 | x: 0, 6 | size: 0, 7 | move: false, 8 | cursorX: 0, 9 | k: 1, 10 | }, 11 | verticalBar: { 12 | y: 0, 13 | size: 0, 14 | move: false, 15 | cursorY: 0, 16 | k: 1, 17 | }, 18 | } 19 | }, 20 | created() { 21 | this.$on('scroll', () => { 22 | this.horizontalBar.x = -parseInt(this.offset.x * this.horizontalBar.k, 10) 23 | this.verticalBar.y = -parseInt(this.offset.y * this.verticalBar.k, 10) 24 | }) 25 | }, 26 | mounted() { 27 | // document.addEventListener('mouseup', () => { 28 | // this.horizontalBar.move = false 29 | // this.verticalBar.move = false 30 | // }) 31 | // document.addEventListener('mousemove', (e) => { 32 | // if (this.verticalBar.move) { 33 | // const height = this.maxPoint.y - this.verticalBar.size 34 | // const moveHeight = this.verticalBar.y + (e.screenY - this.verticalBar.cursorY) 35 | // if (moveHeight > 0 && moveHeight < height) { 36 | // this.verticalBar.y += e.screenY - this.verticalBar.cursorY 37 | // } else if (moveHeight <= 0) { 38 | // this.verticalBar.y = 0 39 | // } else { 40 | // this.verticalBar.y = height 41 | // } 42 | // this.verticalBar.cursorY = e.screenY 43 | // this.offset.y = -this.verticalBar.y / this.verticalBar.k 44 | // requestAnimationFrame(this.rePainted) 45 | // } 46 | // if (this.horizontalBar.move) { 47 | // let width = 0 48 | // if (this.fillWidth > 0) { 49 | // width = this.maxPoint.x - this.horizontalBar.size 50 | // } else { 51 | // width = (this.maxPoint.x + this.fixedWidth) - this.horizontalBar.size 52 | // } 53 | // const moveWidth = this.horizontalBar.x + (e.screenX - this.horizontalBar.cursorX) 54 | // if (moveWidth > 0 && moveWidth < width) { 55 | // this.horizontalBar.x += e.screenX - this.horizontalBar.cursorX 56 | // } else if (moveWidth <= 0) { 57 | // this.horizontalBar.x = 0 58 | // } else { 59 | // this.horizontalBar.x = width 60 | // } 61 | // this.horizontalBar.cursorX = e.screenX 62 | // this.offset.x = -this.horizontalBar.x / this.horizontalBar.k 63 | // requestAnimationFrame(this.rePainted) 64 | // } 65 | // }) 66 | }, 67 | methods: { 68 | scroll(e, type) { 69 | if (type && this.verticalBar.size) { 70 | if (e.offsetY < this.verticalBar.y) { 71 | let k = 15 72 | if (this.verticalBar.y - e.offsetY < 15) { 73 | k = this.verticalBar.y - e.offsetY 74 | } 75 | this.verticalBar.y -= k 76 | this.offset.y = -this.verticalBar.y / this.verticalBar.k 77 | requestAnimationFrame(this.rePainted) 78 | } else if (e.offsetY > this.verticalBar.y + this.verticalBar.size) { 79 | let k = 15 80 | if (e.offsetY - this.verticalBar.y - this.verticalBar.size < 15) { 81 | k = e.offsetY - this.verticalBar.y - this.verticalBar.size 82 | } 83 | this.verticalBar.y += k 84 | this.offset.y = -this.verticalBar.y / this.verticalBar.k 85 | requestAnimationFrame(this.rePainted) 86 | } 87 | } 88 | if (type === 0 && this.horizontalBar.size) { 89 | if (e.offsetX < this.horizontalBar.x) { 90 | let k = 15 91 | if (this.horizontalBar.x - e.offsetX < 15) { 92 | k = this.horizontalBar.x - e.offsetX 93 | } 94 | this.horizontalBar.x -= k 95 | this.offset.x = -this.horizontalBar.x / this.horizontalBar.k 96 | requestAnimationFrame(this.rePainted) 97 | } else if (e.offsetX > this.horizontalBar.x + this.horizontalBar.size) { 98 | let k = 15 99 | if (e.offsetX - this.horizontalBar.x - this.horizontalBar.size < 15) { 100 | k = e.offsetX - this.horizontalBar.x - this.horizontalBar.size 101 | } 102 | this.horizontalBar.x += k 103 | this.horizontalBar.x += 15 104 | this.offset.x = -this.horizontalBar.x / this.horizontalBar.k 105 | requestAnimationFrame(this.rePainted) 106 | } 107 | } 108 | }, 109 | dragMove(e, type) { 110 | if (type) { 111 | this.verticalBar.move = true 112 | this.verticalBar.cursorY = e.screenY 113 | } else { 114 | this.horizontalBar.move = true 115 | this.horizontalBar.cursorX = e.screenX 116 | } 117 | }, 118 | resetScrollBar({ x, y }, bodyWidth, bodyHeight, fixedWidth) { 119 | let width = 0 120 | if (this.fillWidth > 0) { 121 | width = x 122 | } else { 123 | width = x + fixedWidth 124 | } 125 | 126 | const horizontalRatio = width / bodyWidth 127 | if (horizontalRatio >= 1) { 128 | this.horizontalBar.size = 0 129 | } else { 130 | this.horizontalBar.size = width - ((bodyWidth - width) * horizontalRatio) 131 | } 132 | this.horizontalBar.k = horizontalRatio 133 | 134 | let verticalRatio = y / bodyHeight 135 | if (verticalRatio > 1) { 136 | this.verticalBar.size = 0 137 | } else { 138 | this.verticalBar.size = y - ((bodyHeight - y) * verticalRatio) 139 | if (this.verticalBar.size < 30) { 140 | this.verticalBar.size = 30 141 | verticalRatio = (y - 30) / (bodyHeight - y) 142 | } 143 | } 144 | this.verticalBar.k = verticalRatio 145 | 146 | if (width - this.horizontalBar.size < -this.offset.x * this.horizontalBar.k) { 147 | this.offset.x = width - this.bodyWidth 148 | } 149 | if (this.verticalBar.k > 1) { 150 | this.offset.y = 0 151 | } else if (this.maxPoint.y - this.verticalBar.size < -this.offset.y * this.verticalBar.k) { 152 | this.offset.y = this.maxPoint.y - this.bodyHeight 153 | } 154 | this.horizontalBar.x = -this.offset.x * this.horizontalBar.k 155 | this.verticalBar.y = -this.offset.y * this.verticalBar.k 156 | }, 157 | }, 158 | } 159 | -------------------------------------------------------------------------------- /src/grid/painted.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | data() { 4 | const oncheck = new Image() 5 | oncheck.src = require('./oncheck.png') 6 | const offcheck = new Image() 7 | offcheck.src = require('./offcheck.png') 8 | const more = new Image() 9 | more.src = require('./more.png') 10 | return { 11 | headerColor: '#333333', 12 | textColor: '#666666', 13 | borderColor: '#d4d4d4', 14 | white: '#ffffff', 15 | shadowColor: 'rgba(0,0,0,0.2)', 16 | fillColor: '#f9f9f9', 17 | buttonColor: '#20a0ff', 18 | focusColor: '#4285f4', 19 | selectColor: '#6bc9ff', 20 | selectAreaColor: 'rgba(160, 195, 255, 0.2)', 21 | selectRowColor: '#f6f6f6', 22 | dotColor: '#74d337', 23 | oncheck, 24 | offcheck, 25 | more, 26 | } 27 | }, 28 | methods: { 29 | initCanvas() { 30 | const canvas = this.$refs.canvas 31 | let ctx = '' 32 | if (this.ctx) { 33 | ctx = this.ctx 34 | } else { 35 | ctx = canvas.getContext('2d') 36 | this.ctx = ctx 37 | } 38 | ctx.font = 'normal 12px PingFang SC' 39 | const backingStore = ctx.backingStorePixelRatio || 40 | ctx.webkitBackingStorePixelRatio || 41 | ctx.mozBackingStorePixelRatio || 42 | ctx.msBackingStorePixelRatio || 43 | ctx.oBackingStorePixelRatio || 44 | ctx.backingStorePixelRatio || 1 45 | 46 | this.ratio = (window.devicePixelRatio || 1) / backingStore 47 | 48 | this.getAllCells(this.data, this.columns) 49 | this.setBodyHeight(this.allRows, this.originPoint) 50 | this.setMaxpoint(this.width, this.height, this.fixedWidth, this.scrollerWidth) 51 | this.resetScrollBar(this.maxPoint, this.bodyWidth, this.bodyHeight, this.fixedWidth) 52 | }, 53 | p(value) { 54 | const temp = `${value}` 55 | if (temp && temp.indexOf && temp.indexOf('.') === -1) { 56 | return value + 0.5 57 | } 58 | return value 59 | }, 60 | i(value) { 61 | return Math.round(value) 62 | }, 63 | rePainted() { 64 | let items = this.initDisplayItems() 65 | if (this.autoAddRow) { // 自动增加行,减少行 66 | if (items.displayRows[items.displayRows.length - 1].rowIndex >= this.allRows.length - 50) { 67 | const startIndex = this.data.length 68 | for (let i = 0; i < 100; i += 1) { 69 | this.data.push(this.templateData) 70 | } 71 | this.setAllCells(startIndex) 72 | items = this.initDisplayItems() 73 | } else if (this.data.length > this.initRows && items.displayRows[items.displayRows.length - 1].rowIndex <= this.allRows.length - 200) { 74 | this.data.splice(this.data.length - 100, 100) 75 | this.allCells.splice(this.allCells.length - 100, 100) 76 | this.allRows.splice(this.allRows.length - 100, 100) 77 | this.setBodyHeight(this.allRows, this.originPoint) 78 | this.resetScrollBar(this.maxPoint, this.bodyWidth, this.bodyHeight, this.fixedWidth) 79 | items = this.initDisplayItems() 80 | } 81 | } 82 | this.clearPainted() 83 | this.painted(items) 84 | return items 85 | }, 86 | clearPainted() { 87 | this.ctx.clearRect(0, 0, this.width, this.height) 88 | }, 89 | painted(displayItems) { 90 | const ctx = this.ctx 91 | const { displayColumns, displayRows, displayCells, displayFixedCells } = displayItems 92 | 93 | ctx.fillStyle = this.headerColor// text color 94 | ctx.textAlign = 'center' 95 | ctx.lineWidth = 1 96 | ctx.strokeStyle = this.borderColor 97 | ctx.textBaseline = 'middle' 98 | ctx.save() 99 | 100 | this.renderButtons = [] 101 | 102 | this.paintLine(ctx, displayRows, displayColumns) 103 | 104 | this.paintBody(ctx, displayCells) 105 | 106 | if (this.isSelect) { 107 | this.paintSelect(ctx, this.selectArea) 108 | } 109 | if (this.isFocus) { 110 | this.paintFocus(ctx, this.focusCell) 111 | } 112 | 113 | this.paintHeader(ctx, displayColumns) 114 | 115 | if (this.showCheckbox) { 116 | this.paintCheckbox(ctx, displayRows) 117 | } 118 | 119 | this.paintSerial(ctx, displayRows) 120 | 121 | this.paintNo(ctx) 122 | 123 | if (displayFixedCells.length > 0 && this.fillWidth === 0) { 124 | this.paintFixedCells(ctx, displayFixedCells, displayColumns) 125 | } 126 | 127 | this.painScroller(ctx, this.scrollerWidth) 128 | }, 129 | paintCheckbox(ctx, displayRows) { 130 | this.checkboxs = [] 131 | const { i, p, offset, maxPoint, allRows, focusCell, rowFocus, checkboxWidth, rowHeight, serialWidth, originPoint, height, oncheck, offcheck, selected } = this 132 | ctx.fillStyle = this.fillColor 133 | ctx.save() 134 | if (offset.x !== 0) { 135 | ctx.shadowBlur = 10 136 | ctx.shadowColor = this.shadowColor 137 | } 138 | ctx.fillRect(i(0), p(0), i(checkboxWidth + serialWidth), i(height)) 139 | ctx.restore() 140 | ctx.beginPath() 141 | ctx.strokeStyle = this.borderColor 142 | ctx.lineWidth = 1 143 | ctx.moveTo(p(serialWidth), i(0)) 144 | ctx.lineTo(p(serialWidth), i(maxPoint.y)) 145 | ctx.moveTo(p(serialWidth + checkboxWidth), i(0)) 146 | ctx.lineTo(p(serialWidth + checkboxWidth), i(maxPoint.y)) 147 | 148 | for (const item of displayRows) { 149 | if (15 + item.y > -item.height) { 150 | if (rowFocus && rowFocus.cellY === item.y) { 151 | ctx.fillStyle = this.selectRowColor 152 | ctx.fillRect(p(serialWidth), p(item.y), i(checkboxWidth - 1), i(item.height)) 153 | } 154 | ctx.moveTo(p(serialWidth), p(item.y + item.height)) 155 | ctx.lineTo(p(serialWidth + checkboxWidth), p(item.y + item.height)) 156 | if (selected.indexOf(item.rowIndex) !== -1) { 157 | ctx.drawImage(oncheck, p(serialWidth + 5), p(item.y + 5), 20, 20) 158 | } else { 159 | ctx.drawImage(offcheck, p(serialWidth + 5), p(item.y + 5), 20, 20) 160 | } 161 | this.checkboxs.push({ 162 | rowIndex: item.rowIndex, 163 | x: p(serialWidth + 5), 164 | y: p(item.y + 5), 165 | width: 20, 166 | height: 20, 167 | }) 168 | } 169 | } 170 | ctx.stroke() 171 | if (this.focusCell) { 172 | ctx.beginPath() 173 | ctx.strokeStyle = this.focusColor 174 | ctx.lineWidth = 2 175 | ctx.moveTo(i(serialWidth + checkboxWidth), i(focusCell.y - 1)) 176 | ctx.lineTo(i(serialWidth + checkboxWidth), i(focusCell.y + focusCell.height + 1)) 177 | ctx.stroke() 178 | } 179 | ctx.beginPath() 180 | ctx.strokeStyle = this.borderColor 181 | ctx.fillStyle = this.fillColor 182 | ctx.lineWidth = 1 183 | ctx.fillRect(p(serialWidth + 1), p(0), i(checkboxWidth), i(rowHeight)) 184 | ctx.moveTo(p(serialWidth), p(originPoint.y)) 185 | ctx.lineTo(p(serialWidth + checkboxWidth), p(originPoint.y)) 186 | ctx.lineTo(p(serialWidth + checkboxWidth), p(0)) 187 | ctx.stroke() 188 | if (selected.length === allRows.length) { 189 | ctx.drawImage(oncheck, p(serialWidth + 5), p(5), 20, 20) 190 | } else { 191 | ctx.drawImage(offcheck, p(serialWidth + 5), p(5), 20, 20) 192 | } 193 | }, 194 | paintLine(ctx, displayRows, displayColumns) { 195 | const { p, i, maxPoint, rowHeight, rowFocus, serialWidth, bodyHeight } = this 196 | 197 | for (const item of displayRows) { 198 | if (rowFocus && rowFocus.cellY === item.y) { 199 | ctx.fillStyle = this.selectRowColor 200 | ctx.fillRect(p(-1), p(item.y), i(maxPoint.x), i(item.height)) 201 | } 202 | } 203 | ctx.beginPath() 204 | ctx.strokeStyle = this.borderColor 205 | ctx.lineWidth = 1 206 | for (const column of displayColumns) { 207 | if (!column.fixed) { 208 | ctx.moveTo(p(column.x + column.width) + 0.25, i(0)) 209 | ctx.lineTo(p(column.x + column.width), i(bodyHeight)) 210 | } 211 | } 212 | for (const item of displayRows) { 213 | ctx.moveTo(i(0), p(item.y + item.height)) 214 | ctx.lineTo(i(maxPoint.x), p(item.y + item.height)) 215 | } 216 | ctx.moveTo(p(serialWidth), p(rowHeight)) 217 | ctx.lineTo(p(serialWidth), i(bodyHeight)) 218 | ctx.moveTo(i(0), p(rowHeight)) 219 | ctx.lineTo(i(maxPoint.x), p(rowHeight)) 220 | 221 | ctx.stroke() 222 | }, 223 | paintFixedCells(ctx, displayFixedCells, displayColumns) { 224 | const { bodyHeight, rowHeight, maxPoint, paintText, paintButton, p, i, allColumns, fixedWidth, fixedColumns, rowFocus } = this 225 | ctx.save() 226 | const lastDisplayColumn = displayColumns[displayColumns.length - 1] 227 | if (lastDisplayColumn.cellIndex === allColumns.length - 1 - fixedColumns.length) { 228 | if (lastDisplayColumn.x + lastDisplayColumn.width > maxPoint.x) { 229 | ctx.shadowBlur = 10 230 | ctx.shadowColor = this.shadowColor 231 | } 232 | } else { 233 | ctx.shadowBlur = 10 234 | ctx.shadowColor = this.shadowColor 235 | } 236 | ctx.fillStyle = this.white 237 | ctx.fillRect(p(maxPoint.x), p(0), i(fixedWidth + 1), i(bodyHeight)) 238 | ctx.restore() 239 | 240 | ctx.beginPath() 241 | ctx.fillStyle = this.textColor 242 | ctx.strokeStyle = this.borderColor 243 | ctx.lineWidth = 1 244 | let cellX = maxPoint.x 245 | for (const rows of displayFixedCells.reverse()) { 246 | let width = 0 247 | for (const item of rows) { 248 | width = item.width 249 | if (rowFocus && rowFocus.cellY === item.y) { 250 | ctx.fillStyle = this.selectRowColor 251 | ctx.fillRect(p(cellX), p(item.y), i(this.maxPoint.x), i(item.height)) 252 | } 253 | if (item.buttons) { 254 | paintButton(ctx, item, cellX) 255 | } else if (item.paintText && item.paintText.length > 0) { 256 | paintText(ctx, i(cellX + (item.width / 2)), i(15 + item.y), item.paintText) 257 | } 258 | ctx.moveTo(p(cellX), p(item.y)) 259 | ctx.lineTo(p(cellX), p(item.y + item.height)) 260 | ctx.lineTo(p(cellX + item.width), p(item.y + item.height)) 261 | } 262 | cellX += width 263 | } 264 | ctx.stroke() 265 | 266 | ctx.beginPath() 267 | ctx.font = 'bold 12px PingFang SC' 268 | let columnX = maxPoint.x 269 | for (const column of fixedColumns) { 270 | let textColor = this.headerColor 271 | if (rowFocus && rowFocus.cellX === columnX) { 272 | ctx.fillStyle = this.selectRowColor 273 | ctx.fillRect(columnX, 0, column.width, rowHeight) 274 | textColor = this.focusColor 275 | } else { 276 | ctx.fillStyle = this.fillColor 277 | ctx.fillRect(columnX, 0, column.width, rowHeight) 278 | } 279 | ctx.fillStyle = textColor 280 | ctx.fillText(column.title, i(columnX + (column.width / 2)), 15) 281 | ctx.moveTo(p(columnX), p(0)) 282 | ctx.lineTo(p(columnX), p(rowHeight)) 283 | ctx.lineTo(p(columnX + column.width), p(rowHeight)) 284 | columnX += column.width 285 | } 286 | ctx.stroke() 287 | }, 288 | paintButton(ctx, item, cellX) { 289 | let buttonGroupWidth = 0 290 | for (const button of item.buttons) { 291 | buttonGroupWidth += ctx.measureText(button.title).width 292 | } 293 | if (item.buttons.length > 1) { 294 | buttonGroupWidth += 20 * (item.buttons.length - 1) 295 | } 296 | let startX = 0 297 | if (item.width - buttonGroupWidth > 0) { 298 | startX = (item.width - buttonGroupWidth) / 2 299 | } else { 300 | startX = 0 301 | } 302 | ctx.save() 303 | ctx.font = 'normal 12px PingFang SC' 304 | for (const button of item.buttons) { 305 | const buttonWidth = ctx.measureText(button.title).width 306 | ctx.fillStyle = button.color ? button.color : this.buttonColor 307 | ctx.fillText(button.title, startX + cellX + (buttonWidth / 2), item.y + 15) 308 | this.renderButtons.push({ 309 | x: startX + cellX, 310 | y: item.y + 7.5, 311 | cellX, 312 | cellY: item.y, 313 | width: buttonWidth, 314 | height: 12, 315 | click: button.click, 316 | rowIndex: item.rowIndex, 317 | offset: { ...this.offset }, 318 | fixed: item.fixed, 319 | }) 320 | startX += buttonWidth + 20 321 | } 322 | ctx.restore() 323 | }, 324 | paintNo(ctx) { 325 | const { p, rowHeight, serialWidth, more } = this 326 | ctx.beginPath() 327 | ctx.strokeStyle = this.borderColor 328 | ctx.fillStyle = this.fillColor 329 | ctx.fillRect(0, 0, serialWidth, rowHeight) 330 | ctx.fillStyle = this.headerColor 331 | ctx.fillText('序号', serialWidth / 2, 15) 332 | ctx.lineWidth = 1 333 | ctx.moveTo(p(serialWidth), p(0)) 334 | ctx.lineTo(p(serialWidth), p(rowHeight)) 335 | ctx.lineTo(p(0), p(rowHeight)) 336 | ctx.stroke() 337 | 338 | if (this.columnSet) { 339 | ctx.drawImage(more, 50, 6, 18, 18) 340 | } 341 | }, 342 | painScroller(ctx, height) { 343 | const p = this.p 344 | ctx.fillStyle = this.white 345 | ctx.fillRect((this.width - height) + 1, 0, height - 1, this.height) 346 | ctx.fillRect(0, (this.height - height) + 1, this.width, height - 1) 347 | ctx.beginPath() 348 | ctx.lineWidth = 1 349 | ctx.strokeStyle = this.borderColor 350 | ctx.moveTo(p((this.width - height) + 1), p(0)) 351 | ctx.lineTo(p((this.width - height) + 1), p((this.height - height) + 1)) 352 | ctx.lineTo(p(0), p((this.height - height) + 1)) 353 | ctx.fillStyle = this.white 354 | ctx.fillRect(p((this.width - height) + 1), p((this.height - height) + 1), height - 1, height - 1) 355 | ctx.stroke() 356 | }, 357 | paintSelect(ctx, area) { 358 | const { p, originPoint, maxPoint } = this 359 | if (area.x + area.width > originPoint.x && area.y + area.height > originPoint.y && area.x < maxPoint.x && area.y < maxPoint.y) { 360 | ctx.beginPath() 361 | ctx.lineWidth = 1 362 | ctx.strokeStyle = this.selectColor 363 | ctx.moveTo(p(area.x), p(area.y)) 364 | ctx.lineTo(p(area.x + area.width), p(area.y)) 365 | ctx.lineTo(p(area.x + area.width), p(area.y + area.height)) 366 | ctx.lineTo(p(area.x), p(area.y + area.height)) 367 | ctx.closePath() 368 | ctx.fillStyle = this.selectAreaColor 369 | ctx.fill() 370 | ctx.stroke() 371 | } 372 | }, 373 | paintFocus(ctx, cell) { 374 | const { i, originPoint, maxPoint } = this 375 | if (cell.x + cell.width > originPoint.x && cell.y + cell.height > originPoint.y && cell.x < maxPoint.x && cell.y < maxPoint.y) { 376 | ctx.lineWidth = 2 377 | ctx.strokeStyle = this.focusColor 378 | ctx.strokeRect(i(cell.x), i(cell.y), cell.width, cell.height) 379 | } 380 | }, 381 | paintSerial(ctx, displayRows) { 382 | const { i, p, offset, bodyHeight, focusCell, rowFocus, serialWidth } = this 383 | if (!this.showCheckbox) { 384 | ctx.fillStyle = this.fillColor 385 | ctx.save() 386 | if (offset.x !== 0) { 387 | ctx.shadowBlur = 10 388 | ctx.shadowOffsetX = 3 389 | ctx.shadowColor = this.shadowColor 390 | } 391 | ctx.fillRect(0, 0, serialWidth, bodyHeight) 392 | ctx.restore() 393 | } 394 | 395 | ctx.lineWidth = 1 396 | for (const item of displayRows) { 397 | if (15 + item.y > -item.height) { 398 | ctx.beginPath() 399 | ctx.strokeStyle = this.borderColor 400 | let textColor = this.textColor 401 | if (rowFocus && rowFocus.cellY === item.y) { 402 | ctx.fillStyle = this.selectRowColor 403 | ctx.fillRect(-1, item.y + 1, serialWidth + 1, item.height) 404 | textColor = this.focusColor 405 | } 406 | ctx.fillStyle = textColor 407 | 408 | ctx.fillText(`${item.rowIndex + 1}`, serialWidth / 2, 15 + item.y) 409 | ctx.moveTo(p(0), p(item.y + item.height)) 410 | ctx.lineTo(p(serialWidth), p(item.y + item.height)) 411 | ctx.stroke() 412 | 413 | if (item.showDot) { 414 | ctx.beginPath() 415 | ctx.fillStyle = this.dotColor 416 | ctx.strokeStyle = this.fillColor 417 | ctx.arc(15, 15 + item.y, 4, 0, 2 * Math.PI) 418 | ctx.fill() 419 | ctx.stroke() 420 | } 421 | } 422 | } 423 | ctx.stroke() 424 | 425 | if (this.focusCell && !this.showCheckbox) { 426 | ctx.beginPath() 427 | ctx.strokeStyle = this.focusColor 428 | ctx.lineWidth = 2 429 | ctx.moveTo(i(serialWidth), i(focusCell.y - 1)) 430 | ctx.lineTo(i(serialWidth), i(focusCell.y + focusCell.height + 1)) 431 | ctx.stroke() 432 | } 433 | }, 434 | paintHeader(ctx, displayColumns) { 435 | const { p, i, focusCell, width, rowHeight, rowFocus } = this 436 | ctx.fillStyle = this.fillColor 437 | ctx.fillRect(0, 0, width, rowHeight) 438 | ctx.beginPath() 439 | ctx.strokeStyle = this.borderColor 440 | ctx.font = 'bold 12px PingFang SC' 441 | ctx.lineWidth = 1 442 | for (const column of displayColumns) { 443 | if (!column.fixed || this.fillWidth > 0) { 444 | let textColor = this.headerColor 445 | if (rowFocus && rowFocus.cellX === column.x) { 446 | ctx.fillStyle = this.selectRowColor 447 | ctx.fillRect(p(column.x), p(0), p(column.width), p(rowHeight - 1)) 448 | textColor = this.focusColor 449 | } 450 | ctx.fillStyle = textColor 451 | ctx.fillText(column.title, p(column.x + (column.width / 2)), p(15)) 452 | ctx.moveTo(p(column.x + column.width), p(0)) 453 | ctx.lineTo(p(column.x + column.width), p(rowHeight)) 454 | } 455 | } 456 | ctx.stroke() 457 | 458 | if (focusCell) { 459 | ctx.beginPath() 460 | ctx.strokeStyle = this.focusColor 461 | ctx.lineWidth = 2 462 | ctx.moveTo(i(focusCell.x - 1), i(rowHeight)) 463 | ctx.lineTo(i(focusCell.x + focusCell.width + 1), i(rowHeight)) 464 | ctx.stroke() 465 | } 466 | }, 467 | paintBody(ctx, displayCells) { 468 | const { paintText, i } = this 469 | ctx.beginPath() 470 | ctx.font = 'normal 12px PingFang SC' 471 | ctx.fillStyle = this.textColor 472 | for (const rows of displayCells) { 473 | for (const item of rows) { 474 | if (!item.fixed || this.fillWidth > 0) { 475 | if (item.buttons) { 476 | this.paintButton(ctx, item, i(item.x)) 477 | } else if (item.paintText && item.paintText.length > 0) { 478 | paintText(ctx, i(item.x + (item.width / 2)), i(15 + item.y), item.paintText) 479 | } 480 | } 481 | } 482 | } 483 | ctx.stroke() 484 | }, 485 | paintText(ctx, x, y, row) { 486 | for (let b = 0; b < row.length; b += 1) { 487 | ctx.fillText(row[b], x, y + (b * 18)) 488 | } 489 | }, 490 | }, 491 | } 492 | -------------------------------------------------------------------------------- /src/grid/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 471 | 472 | 638 | 639 | -------------------------------------------------------------------------------- /src/grid/events.js: -------------------------------------------------------------------------------- 1 | import throttle from 'throttle-debounce/throttle' 2 | 3 | export default { 4 | data() { 5 | return { 6 | rowFocus: null, 7 | isFirefox: false, 8 | pixelRatio: 1, 9 | shiftDown: false, 10 | ctrlDown: false, 11 | } 12 | }, 13 | created() { 14 | this.isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1 15 | }, 16 | watch: { 17 | retract() { 18 | this.handleResize() 19 | }, 20 | }, 21 | mounted() { 22 | this.pixelRatio = window.devicePixelRatio 23 | }, 24 | methods: { 25 | removeEvent() { 26 | window.removeEventListener('mousedown', this.handleMousedown, false) 27 | window.removeEventListener('mousemove', throttle(16, this.handleMousemove), true) 28 | window.removeEventListener('mouseup', this.handleMouseup, false) 29 | window.removeEventListener('resize', this.handleResize, false) 30 | window.removeEventListener(this.isFirefox ? 'DOMMouseScroll' : 'mousewheel', this.handleWheel) 31 | window.removeEventListener('keydown', this.handleKeydown, false) 32 | window.removeEventListener('keyup', this.handleKeyup, false) 33 | }, 34 | initEvent() { 35 | window.addEventListener('mousedown', this.handleMousedown, false) 36 | window.addEventListener('mousemove', throttle(16, this.handleMousemove), true) 37 | window.addEventListener('mouseup', this.handleMouseup, false) 38 | // this.$refs.canvas.addEventListener('mouseleave', this.handleMouseup, false) 39 | this.$refs.canvas.addEventListener('dblclick', this.handleDoubleClick, false) 40 | this.$refs.canvas.addEventListener('click', this.handleClick, false) 41 | window.addEventListener('resize', this.handleResize, false) 42 | window.addEventListener(this.isFirefox ? 'DOMMouseScroll' : 'mousewheel', this.handleWheel) 43 | window.addEventListener('keydown', this.handleKeydown, false) 44 | window.addEventListener('keyup', this.handleKeyup, false) 45 | }, 46 | handleResize() { 47 | this.isFocus = false 48 | this.focusCell = null 49 | 50 | this.selectArea = null 51 | this.isSelect = false 52 | this.save() 53 | this.hideInput() 54 | this.initSize() 55 | }, 56 | handleColumnSet() { 57 | this.showColumnSet = false 58 | this.initSize() 59 | }, 60 | handleClick(evt) { 61 | if (!this.isSelect) { 62 | const x = evt.offsetX 63 | const y = evt.offsetY 64 | if (x > this.maxPoint.x && y > this.maxPoint.y && x < this.width && y < this.height) { 65 | this.fullScreen() 66 | } 67 | if (this.showCheckbox) { 68 | if (x > this.serialWidth && x < this.originPoint.x) { 69 | const checkbox = this.getCheckboxAt(x, y) 70 | if (checkbox) { 71 | if (this.selected.indexOf(checkbox.rowIndex) !== -1) { 72 | this.selected.splice(this.selected.findIndex((item) => { if (item === checkbox.rowIndex) { return true } return false }), 1) 73 | } else { 74 | this.selected.push(checkbox.rowIndex) 75 | } 76 | this.rePainted() 77 | } else if (x > this.serialWidth + 5 && x < this.serialWidth + 5 + 20 && y > 5 && y < 25) { 78 | if (this.selected.length === this.allRows.length) { 79 | this.selected = [] 80 | } else { 81 | this.selected = [] 82 | for (const row of this.allRows) { 83 | this.selected.push(row.rowIndex) 84 | } 85 | } 86 | this.rePainted() 87 | } 88 | return 89 | } 90 | } 91 | 92 | if (this.columnSet) { 93 | if (!this.showColumnSet) { 94 | if (x > 55 && x < 64 && y > 7 && y < 23) { 95 | this.showColumnSet = true 96 | return 97 | } 98 | } else { 99 | this.handleColumnSet() 100 | } 101 | } 102 | 103 | const button = this.getButtonAt(x, y) 104 | if (button) { 105 | this.rowFocus = button 106 | button.click(this.data[button.rowIndex], button.rowIndex) 107 | this.rePainted() 108 | } 109 | } 110 | }, 111 | handleDoubleClick() { 112 | if (this.focusCell) { 113 | const { x, y, width, height, content } = this.focusCell 114 | this.$refs.input.innerHTML = content 115 | this.keepLastIndex(this.$refs.input) 116 | this.showInput(x, y, width, height) 117 | } 118 | }, 119 | handleWheel(e) { 120 | if (e.target.tagName === 'CANVAS') { 121 | if (!this.isEditing) { 122 | const { deltaX, deltaY } = e 123 | if (Math.abs(deltaX) > Math.abs(deltaY)) { 124 | const lastScrollX = this.offset.x 125 | let maxWidth = 0 126 | if (this.fillWidth > 0) { 127 | maxWidth = this.maxPoint.x 128 | } else { 129 | maxWidth = this.maxPoint.x + this.fixedWidth 130 | } 131 | if (this.offset.x - deltaX > 0) { 132 | this.offset.x = 0 133 | } else if ((this.bodyWidth - maxWidth) + this.offset.x < deltaX) { 134 | this.offset.x = maxWidth - this.bodyWidth 135 | if (maxWidth - this.bodyWidth < 0) { 136 | this.offset.x = maxWidth - this.bodyWidth 137 | } else { 138 | e.preventDefault() 139 | e.returnValue = false 140 | } 141 | } else { 142 | e.preventDefault() 143 | e.returnValue = false 144 | this.offset.x -= deltaX 145 | } 146 | if (lastScrollX !== this.offset.x) { 147 | requestAnimationFrame(this.rePainted) 148 | this.$emit('scroll') 149 | } 150 | } else { 151 | const lastScrollY = this.offset.y 152 | if (lastScrollY - deltaY > 0) { 153 | this.offset.y = 0 154 | } else if ((this.bodyHeight - this.maxPoint.y) + lastScrollY < deltaY) { 155 | if (this.maxPoint.y - this.bodyHeight < 0) { 156 | this.offset.y = this.maxPoint.y - this.bodyHeight 157 | } else { 158 | e.preventDefault() 159 | e.returnValue = false 160 | } 161 | } else { 162 | e.preventDefault() 163 | e.returnValue = false 164 | this.offset.y -= deltaY 165 | } 166 | if (lastScrollY !== this.offset.y) { 167 | requestAnimationFrame(this.rePainted) 168 | this.$emit('scroll') 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | handleMousemove(evt) { 175 | if (this.isDown && this.isFocus && evt.target.tagName === 'CANVAS') { 176 | const eX = evt.offsetX 177 | const eY = evt.offsetY 178 | const { x, y, width, height, rowIndex, cellIndex } = this.focusCell 179 | if (eX >= x && eX <= x + width && eY >= y && eY <= y + height) { 180 | if (this.selectArea) { 181 | this.selectArea = null 182 | this.isSelect = false 183 | this.rePainted() 184 | } 185 | } else { 186 | const cell = this.getCellAt(eX, eY) 187 | if (cell) { 188 | if (cell.x >= x && cell.y >= y) { 189 | this.selectArea = { x, y, width: (cell.x - x) + cell.width, height: (cell.y - y) + cell.height, cellIndex, rowIndex, offset: { ...this.offset } } 190 | } else if (cell.x >= x && cell.y <= y) { 191 | this.selectArea = { x, y: cell.y, width: (cell.x - x) + cell.width, height: (y - cell.y) + height, rowIndex: cell.rowIndex, cellIndex, offset: { ...this.offset } } 192 | } else if (cell.x <= x && cell.y <= y) { 193 | this.selectArea = { x: cell.x, y: cell.y, width: (x - cell.x) + width, height: (y - cell.y) + height, rowIndex: cell.rowIndex, cellIndex: cell.cellIndex, offset: { ...this.offset } } 194 | } else if (cell.x <= x && cell.y >= y) { 195 | this.selectArea = { x: cell.x, y, width: (x - cell.x) + width, height: (cell.y - y) + cell.height, rowIndex, cellIndex: cell.cellIndex, offset: { ...this.offset } } 196 | } 197 | this.selectArea.rowCount = Math.abs(cell.rowIndex - rowIndex) + 1 198 | this.isSelect = true 199 | this.rePainted() 200 | } 201 | } 202 | } 203 | if (this.verticalBar.move) { 204 | const height = this.maxPoint.y - this.verticalBar.size 205 | const moveHeight = this.verticalBar.y + (evt.screenY - this.verticalBar.cursorY) 206 | if (moveHeight > 0 && moveHeight < height) { 207 | this.verticalBar.y += evt.screenY - this.verticalBar.cursorY 208 | } else if (moveHeight <= 0) { 209 | this.verticalBar.y = 0 210 | } else { 211 | this.verticalBar.y = height 212 | } 213 | this.verticalBar.cursorY = evt.screenY 214 | this.offset.y = -this.verticalBar.y / this.verticalBar.k 215 | requestAnimationFrame(this.rePainted) 216 | } 217 | if (this.horizontalBar.move) { 218 | let width = 0 219 | if (this.fillWidth > 0) { 220 | width = this.maxPoint.x - this.horizontalBar.size 221 | } else { 222 | width = (this.maxPoint.x + this.fixedWidth) - this.horizontalBar.size 223 | } 224 | const moveWidth = this.horizontalBar.x + (evt.screenX - this.horizontalBar.cursorX) 225 | if (moveWidth > 0 && moveWidth < width) { 226 | this.horizontalBar.x += evt.screenX - this.horizontalBar.cursorX 227 | } else if (moveWidth <= 0) { 228 | this.horizontalBar.x = 0 229 | } else { 230 | this.horizontalBar.x = width 231 | } 232 | this.horizontalBar.cursorX = evt.screenX 233 | this.offset.x = -this.horizontalBar.x / this.horizontalBar.k 234 | requestAnimationFrame(this.rePainted) 235 | } 236 | }, 237 | handleMousedown(evt) { 238 | this.save() 239 | let needRepaint = false 240 | if (evt.target.tagName === 'CANVAS') { 241 | setTimeout(() => { 242 | this.isDown = true 243 | this.hideInput() 244 | this.selectArea = null 245 | this.isSelect = false 246 | const eX = evt.offsetX 247 | const eY = evt.offsetY 248 | if (eX > this.originPoint.x && eY > this.rowHeight && eX < this.maxPoint.x) { 249 | const cell = this.getCellAt(eX, eY) 250 | if (cell && !cell.buttons && !cell.readOnly) { 251 | if (this.shiftDown) { 252 | const { x, y, width, height, rowIndex, cellIndex } = this.focusCell 253 | if (eX >= x && eX <= x + width && eY >= y && eY <= y + height) { 254 | this.selectArea = null 255 | this.isSelect = false 256 | this.rePainted() 257 | } else { 258 | if (cell.x >= x && cell.y >= y) { 259 | this.selectArea = { x, y, width: (cell.x - x) + cell.width, height: (cell.y - y) + cell.height, cellIndex, rowIndex, offset: { ...this.offset } } 260 | } else if (cell.x >= x && cell.y <= y) { 261 | this.selectArea = { x, y: cell.y, width: (cell.x - x) + cell.width, height: (y - cell.y) + height, rowIndex: cell.rowIndex, cellIndex, offset: { ...this.offset } } 262 | } else if (cell.x <= x && cell.y <= y) { 263 | this.selectArea = { x: cell.x, y: cell.y, width: (x - cell.x) + width, height: (y - cell.y) + height, rowIndex: cell.rowIndex, cellIndex: cell.cellIndex, offset: { ...this.offset } } 264 | } else if (cell.x <= x && cell.y >= y) { 265 | this.selectArea = { x: cell.x, y, width: (x - cell.x) + width, height: (cell.y - y) + cell.height, rowIndex, cellIndex: cell.cellIndex, offset: { ...this.offset } } 266 | } 267 | this.selectArea.rowCount = Math.abs(cell.rowIndex - rowIndex) + 1 268 | this.isSelect = true 269 | this.rePainted() 270 | } 271 | } else { 272 | this.focusCell = cell 273 | this.rowFocus = { 274 | cellX: cell.x, 275 | cellY: cell.y, 276 | rowIndex: this.focusCell.rowIndex, 277 | offset: { ...this.offset }, 278 | } 279 | this.paintFocusCell(cell) 280 | this.$emit('focus', cell.rowData) 281 | } 282 | } else { 283 | this.isFocus = false 284 | this.focusCell = null 285 | this.rePainted() 286 | } 287 | } else { 288 | this.isFocus = false 289 | this.focusCell = null 290 | this.rePainted() 291 | } 292 | }, 0) 293 | } else if (!evt.target.classList.contains('input-content')) { 294 | if (evt.target.tagName !== 'CANVAS') { 295 | if (this.isEditing) { 296 | this.save() 297 | this.hideInput() 298 | needRepaint = true 299 | } 300 | if (this.isFocus) { 301 | this.isFocus = false 302 | this.focusCell = null 303 | needRepaint = true 304 | } 305 | if (this.isSelect) { 306 | this.selectArea = null 307 | this.isSelect = false 308 | needRepaint = true 309 | } 310 | if (this.showColumnSet) { 311 | setTimeout(() => { 312 | if (evt.path.indexOf(this.$refs.columnSet) === -1) { 313 | this.showColumnSet = false 314 | this.initSize() 315 | } 316 | }, 0) 317 | } 318 | if (needRepaint) { 319 | this.rePainted() 320 | } 321 | } 322 | } 323 | }, 324 | handleMouseup() { 325 | this.isDown = false 326 | this.horizontalBar.move = false 327 | this.verticalBar.move = false 328 | }, 329 | handleKeyup(e) { 330 | if (e.keyCode === 16) { 331 | this.shiftDown = false 332 | } 333 | }, 334 | handleKeydown(e) { 335 | if (this.isFocus) { 336 | if (!this.isEditing) { 337 | if (e.keyCode === 38) { 338 | e.preventDefault() 339 | this.moveFocus('up') 340 | } else if (e.keyCode === 40) { 341 | e.preventDefault() 342 | this.moveFocus('down') 343 | } else if (e.keyCode === 37) { 344 | e.preventDefault() 345 | this.moveFocus('left') 346 | } else if (e.keyCode === 39) { 347 | e.preventDefault() 348 | this.moveFocus('right') 349 | } else if (e.keyCode === 16) { 350 | this.shiftDown = true 351 | } else if (e.keyCode === 8 || e.keyCode === 46) { 352 | if (this.isSelect) { 353 | const selectCells = this.getCellsBySelect(this.selectArea) 354 | const deleteData = [] 355 | for (const row of selectCells) { 356 | const temp = { 357 | rowData: row[0].rowData, 358 | index: row[0].rowIndex, 359 | items: [], 360 | } 361 | for (const item of row) { 362 | if (item.readOnly) { 363 | temp.items.push({ 364 | key: '', 365 | value: '', 366 | }) 367 | } else { 368 | temp.items.push({ 369 | key: item.key, 370 | value: '', 371 | }) 372 | } 373 | } 374 | deleteData.push(temp) 375 | } 376 | this.$emit('update', deleteData) 377 | } else { 378 | this.$emit('updateItem', { 379 | index: this.focusCell.rowIndex, 380 | key: this.focusCell.key, 381 | value: '', 382 | }) 383 | } 384 | } else if (/macintosh|mac os x/i.test(navigator.userAgent)) { 385 | if (e.keyCode === 90 && e.metaKey) { 386 | e.preventDefault() 387 | this.$emit('history_back') 388 | } else if (e.keyCode === 89 && e.metaKey) { 389 | e.preventDefault() 390 | this.$emit('history_forward') 391 | } else if (e.keyCode === 67 && e.metaKey) { 392 | if (this.isSelect) { 393 | e.preventDefault() 394 | this.selectText(this.$refs.inputSelect) 395 | document.execCommand('Copy') 396 | } 397 | } 398 | } else if (e.keyCode === 90 && e.ctrlKey) { 399 | e.preventDefault() 400 | this.$emit('history_back') 401 | } else if (e.keyCode === 89 && e.ctrlKey) { 402 | e.preventDefault() 403 | this.$emit('history_forward') 404 | } else if (e.keyCode === 67 && e.ctrlKey) { 405 | if (this.isSelect) { 406 | e.preventDefault() 407 | this.selectText(this.$refs.inputSelect) 408 | document.execCommand('Copy') 409 | } 410 | } 411 | } 412 | if (e.keyCode === 13) { 413 | this.save() 414 | this.moveFocus('down') 415 | } else if (e.keyCode === 27) { 416 | this.hideInput() 417 | this.$refs.input.innerHTML = '' 418 | } else if (e.keyCode === 9) { 419 | this.save() 420 | this.moveFocus('right') 421 | } 422 | } 423 | }, 424 | handleInputKeyup() { 425 | 426 | }, 427 | moveFocus(type) { 428 | if (!this.focusCell) { 429 | return 430 | } 431 | if (this.isSelect) { 432 | this.selectArea = null 433 | this.isSelect = false 434 | } 435 | const row = this.focusCell.rowIndex 436 | const cell = this.focusCell.cellIndex 437 | this.hideInput() 438 | if (type === 'up') { 439 | if (this.getDisplayCellIndexByRowIndex(row) !== 0) { 440 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(this.displayCells, row - 1, cell), { offset: { ...this.offset } }) 441 | if (this.focusCell.y < this.originPoint.y) { 442 | this.offset.y += this.originPoint.y - this.focusCell.y 443 | } 444 | this.paintFocusCell(this.focusCell) 445 | } else { 446 | const rowIndex = this.displayRows[0].rowIndex 447 | if (rowIndex > 0) { 448 | this.offset.y += this.allRows[this.displayRows[0].rowIndex - 1].height 449 | const { displayCells } = this.rePainted() 450 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(displayCells, displayCells[0][0].rowIndex, cell), { offset: { ...this.offset } }) 451 | this.paintFocusCell(this.focusCell) 452 | } 453 | } 454 | } else if (type === 'down') { 455 | if (row !== this.displayCells[this.displayCells.length - 1][0].rowIndex) { 456 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(this.displayCells, row + 1, cell), { offset: { ...this.offset } }) 457 | if (this.focusCell.y + this.focusCell.height > this.maxPoint.y) { 458 | this.offset.y -= (this.focusCell.y + this.focusCell.height) - this.maxPoint.y 459 | } 460 | this.paintFocusCell(this.focusCell) 461 | } else { 462 | const rowIndex = this.displayRows[this.displayRows.length - 1].rowIndex 463 | if (rowIndex < this.allRows.length - 1) { 464 | this.offset.y -= this.allRows[this.displayRows[this.displayRows.length - 1].rowIndex + 1].height 465 | const { displayCells } = this.rePainted() 466 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(displayCells, displayCells[displayCells.length - 1][0].rowIndex, cell), { offset: { ...this.offset } }) 467 | this.paintFocusCell(this.focusCell) 468 | } 469 | } 470 | } else if (type === 'left') { 471 | if (cell !== this.getFirstDisplayCellIndex(this.displayCells)) { 472 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(this.displayCells, row, cell - 1), { offset: { ...this.offset } }) 473 | if (this.focusCell.x < this.originPoint.x) { 474 | this.offset.x += this.originPoint.x - this.focusCell.x 475 | } 476 | this.paintFocusCell(this.focusCell) 477 | } else { 478 | const cellIndex = this.displayColumns[0].cellIndex 479 | if (cellIndex > 0) { 480 | this.offset.x += this.allColumns[cellIndex - 1].width 481 | const { displayCells } = this.rePainted() 482 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(displayCells, row, this.getFirstDisplayCellIndex(displayCells)), { offset: { ...this.offset } }) 483 | this.paintFocusCell(this.focusCell) 484 | } 485 | } 486 | } else if (type === 'right') { 487 | if (cell !== this.getLastDisplayCellIndex(this.displayCells)) { 488 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(this.displayCells, row, cell + 1), { offset: { ...this.offset } }) 489 | if (this.focusCell.x + this.focusCell.width > this.maxPoint.x - this.fixedWidth) { 490 | this.offset.x -= (this.focusCell.x + this.focusCell.width) - (this.maxPoint.x - this.fixedWidth) 491 | } 492 | this.paintFocusCell(this.focusCell) 493 | } else { 494 | const cellIndex = this.displayColumns[this.displayColumns.length - 1 - this.displayFixedCells.length].cellIndex 495 | if (cellIndex < this.allColumns.length - 1) { 496 | this.offset.x -= this.allColumns[cellIndex + 1].width 497 | const { displayCells } = this.rePainted() 498 | this.focusCell = Object.assign({}, this.getDisplayCellByRowIndex(displayCells, row, this.getLastDisplayCellIndex(displayCells)), { offset: { ...this.offset } }) 499 | this.paintFocusCell(this.focusCell) 500 | } 501 | } 502 | } 503 | this.$emit('scroll') 504 | }, 505 | getDisplayCellByRowIndex(displayCells, rowIndex, cellIndex) { 506 | for (const item of displayCells) { 507 | if (item[0].rowIndex === rowIndex) { 508 | for (const cell of item) { 509 | if (cell.cellIndex === cellIndex) { 510 | return cell 511 | } 512 | } 513 | } 514 | } 515 | return null 516 | }, 517 | getDisplayCellIndexByRowIndex(row) { 518 | let index = 0 519 | for (const item of this.displayCells) { 520 | if (item[0].rowIndex === row) { 521 | return index 522 | } 523 | index += 1 524 | } 525 | return null 526 | }, 527 | getLastDisplayCellIndex(displayCells) { 528 | return displayCells[0][displayCells[0].length - 1].cellIndex 529 | }, 530 | getFirstDisplayCellIndex(displayCells) { 531 | return displayCells[0][0].cellIndex 532 | }, 533 | keepLastIndex(obj) { 534 | if (window.getSelection) { // ie11 10 9 ff safari 535 | obj.focus() // 解决ff不获取焦点无法定位问题 536 | const range = window.getSelection()// 创建range 537 | range.selectAllChildren(obj)// range 选择obj下所有子内容 538 | range.collapseToEnd()// 光标移至最后 539 | } else if (document.selection) { // ie10 9 8 7 6 5 540 | const range = document.selection.createRange()// 创建选择对象 541 | // var range = document.body.createTextRange(); 542 | range.moveToElementText(obj)// range定位到obj 543 | range.collapse(false)// 光标移至最后 544 | range.select() 545 | } 546 | }, 547 | selectText(obj) { 548 | if (document.selection) { 549 | const range = document.body.createTextRange() 550 | range.moveToElementText(obj) 551 | range.select() 552 | } else if (window.getSelection) { 553 | const range = document.createRange() 554 | range.selectNodeContents(obj) 555 | window.getSelection().removeAllRanges() 556 | window.getSelection().addRange(range) 557 | } 558 | }, 559 | }, 560 | } 561 | -------------------------------------------------------------------------------- /src/grid/calculate.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | const rowHeight = 30 4 | const serialWidth = 70 5 | const checkboxWidth = 30 6 | const scrollerWidth = 20 7 | const height = this.leftHeight ? window.innerHeight - this.leftHeight : 500 8 | let originPointX = serialWidth 9 | if (this.showCheckbox) { 10 | originPointX += checkboxWidth 11 | } 12 | return { 13 | width: 0, 14 | height, 15 | rowHeight, 16 | scrollerWidth, 17 | fixedWidth: 0, 18 | bodyWidth: 0, 19 | bodyHeight: 0, 20 | serialWidth, 21 | checkboxWidth: 30, 22 | fillWidth: 0, 23 | 24 | allCells: [], 25 | displayCells: [], 26 | allRows: [], 27 | displayRows: [], 28 | allColumns: [], 29 | displayColumns: [], 30 | allFixedCells: [], 31 | displayFixedCells: [], 32 | fixedColumns: [], 33 | renderButtons: [], 34 | checkboxs: [], 35 | selected: [], 36 | 37 | offset: { 38 | x: 0, 39 | y: 0, 40 | }, 41 | originPoint: { 42 | x: originPointX, 43 | y: rowHeight, 44 | }, 45 | maxPoint: { 46 | x: 0, 47 | y: height - scrollerWidth, 48 | }, 49 | } 50 | }, 51 | watch: { 52 | showCheckbox() { 53 | this.initSize() 54 | }, 55 | leftHeight() { 56 | this.initSize() 57 | }, 58 | }, 59 | mounted() { 60 | this.width = this.$refs.grid.offsetWidth - 2 61 | 62 | this.height = this.leftHeight ? window.innerHeight - this.leftHeight : 500 63 | this.maxPoint.y = this.height - this.scrollerWidth 64 | 65 | this.bodyWidth = this.originPoint.x 66 | for (const column of this.columns) { 67 | this.bodyWidth += column.width ? column.width : 100 68 | } 69 | if (this.bodyWidth < this.width - this.scrollerWidth) { 70 | this.fillWidth = (this.width - this.bodyWidth - this.scrollerWidth) / this.columns.length 71 | this.bodyWidth = this.width - this.scrollerWidth 72 | } 73 | }, 74 | methods: { 75 | fullScreen() { 76 | // TODO fullscrean 77 | 78 | // this.$refs.canvas.style.position = 'fixed' 79 | // this.$refs.canvas.style.top = 0 80 | // this.$refs.canvas.style.left = 0 81 | // this.$refs.canvas.style.right = 0 82 | // this.$refs.canvas.style.bottom = 0 83 | // this.$refs.canvas.style.zIndex = 1000 84 | 85 | // this.width = window.innerWidth 86 | // this.height = window.innerHeight 87 | 88 | // if (this.showCheckbox) { 89 | // this.selected = [...this.initSelected] 90 | // this.originPoint.x = this.serialWidth + this.checkboxWidth 91 | // this.bodyWidth += this.checkboxWidth 92 | // } else { 93 | // this.originPoint.x = this.serialWidth 94 | // this.bodyWidth -= this.checkboxWidth 95 | // } 96 | // this.bodyWidth = this.originPoint.x 97 | // let columnCount = 0 98 | // for (const column of this.allColumns) { 99 | // if (column.checked) { 100 | // this.bodyWidth += column.width ? column.width : 100 101 | // columnCount += 1 102 | // } 103 | // } 104 | // this.fillWidth = 0 105 | // if (this.bodyWidth < this.width - this.scrollerWidth) { 106 | // this.fillWidth = (this.width - this.bodyWidth - this.scrollerWidth) / columnCount 107 | // this.bodyWidth = this.width - this.scrollerWidth 108 | // } 109 | 110 | // this.setBodyHeight(this.allRows, this.originPoint) 111 | // this.setFixedWidth(this.allColumns, this.fillWidth) 112 | // this.setMaxpoint(this.width, this.height, this.fixedWidth, this.scrollerWidth, this.fillWidth) 113 | // this.resetScrollBar(this.maxPoint, this.bodyWidth, this.bodyHeight, this.fixedWidth) 114 | // requestAnimationFrame(this.rePainted) 115 | }, 116 | initSize() { 117 | if (this.$refs.grid) { 118 | this.width = this.$refs.grid.offsetWidth - 2 119 | this.height = this.leftHeight ? window.innerHeight - this.leftHeight : 500 120 | 121 | if (this.showCheckbox) { 122 | if (this.initSelected) { 123 | this.selected = [...this.initSelected] 124 | } 125 | this.originPoint.x = this.serialWidth + this.checkboxWidth 126 | this.bodyWidth += this.checkboxWidth 127 | } else { 128 | this.originPoint.x = this.serialWidth 129 | this.bodyWidth -= this.checkboxWidth 130 | } 131 | this.bodyWidth = this.originPoint.x 132 | let columnCount = 0 133 | for (const column of this.allColumns) { 134 | if (column.checked) { 135 | this.bodyWidth += column.width ? column.width : 100 136 | columnCount += 1 137 | } 138 | } 139 | this.fillWidth = 0 140 | if (this.bodyWidth < this.width - this.scrollerWidth) { 141 | this.fillWidth = (this.width - this.bodyWidth - this.scrollerWidth) / columnCount 142 | this.bodyWidth = this.width - this.scrollerWidth 143 | } 144 | this.setBodyHeight(this.allRows, this.originPoint) 145 | this.setFixedWidth(this.allColumns, this.fillWidth) 146 | this.setMaxpoint(this.width, this.height, this.fixedWidth, this.scrollerWidth, this.fillWidth) 147 | this.resetScrollBar(this.maxPoint, this.bodyWidth, this.bodyHeight, this.fixedWidth) 148 | requestAnimationFrame(this.rePainted) 149 | } 150 | }, 151 | setFixedWidth(allColumns, fillWidth) { 152 | this.fixedWidth = 0 153 | for (const column of allColumns) { 154 | if (column.checked && column.fixed) { 155 | this.fixedWidth += column.width ? column.width : 100 156 | this.fixedWidth += fillWidth 157 | } 158 | } 159 | }, 160 | setBodyHeight(allRows, { y }) { 161 | this.bodyHeight = y 162 | for (const row of allRows) { 163 | this.bodyHeight += row.height 164 | } 165 | }, 166 | setMaxpoint(width, height, fixedWidth, scrollerWidth, fillWidth) { 167 | if (fillWidth > 0) { 168 | this.maxPoint.x = width - scrollerWidth 169 | } else { 170 | this.maxPoint.x = width - scrollerWidth - fixedWidth 171 | } 172 | this.maxPoint.y = height - scrollerWidth 173 | }, 174 | getAllCells(value, columns) { 175 | this.allCells = [] 176 | this.allRows = [] 177 | this.allColumns = [] 178 | this.allFixedCells = [] 179 | this.fixedColumns = [] 180 | this.fixedWidth = 0 181 | const { rowHeight, ctx, getTextLine, allRows, allCells, allColumns, fixedColumns, allFixedCells } = this 182 | let rowIndex = 0 183 | for (const item of value) { 184 | let maxHeight = rowHeight 185 | let cellIndex = 0 186 | const cellTemp = [] 187 | for (const column of columns) { 188 | if (rowIndex === 0) { 189 | if (column.fixed) { 190 | this.fixedWidth += column.width 191 | fixedColumns.push({ 192 | cellIndex, 193 | ...column, 194 | }) 195 | allColumns.push({ 196 | height: rowHeight, 197 | cellIndex, 198 | ...column, 199 | checked: true, 200 | }) 201 | } else { 202 | allColumns.push({ 203 | height: rowHeight, 204 | cellIndex, 205 | ...column, 206 | checked: true, 207 | }) 208 | } 209 | } 210 | let text = '' 211 | let buttons 212 | let textLine 213 | if (column.renderText) { 214 | text = column.renderText(item) 215 | } else if (column.renderButton) { 216 | buttons = column.renderButton(this.data[rowIndex], rowIndex) 217 | } else { 218 | text = item[column.key] 219 | } 220 | if (text || text === 0) { 221 | textLine = getTextLine(ctx, text, column.width ? column.width : 100) 222 | let textLineCount = 0 223 | if (textLine) { 224 | textLineCount = textLine.length 225 | } 226 | if (textLineCount > 1) { 227 | if (maxHeight < rowHeight + ((textLineCount - 1) * 18)) { 228 | maxHeight = rowHeight + ((textLineCount - 1) * 18) 229 | } 230 | } 231 | } 232 | 233 | if (column.fixed) { 234 | cellTemp.push({ 235 | width: column.width ? column.width : 100, 236 | content: item[column.key], 237 | key: column.key, 238 | rowIndex, 239 | cellIndex, 240 | paintText: textLine, 241 | fixed: column.fixed === true, 242 | readOnly: column.readOnly === true, 243 | buttons, 244 | renderText: column.renderText, 245 | renderButton: column.renderButton, 246 | rowData: item, 247 | type: column.type, 248 | }) 249 | } else { 250 | cellTemp.push({ 251 | width: column.width ? column.width : 100, 252 | content: item[column.key], 253 | key: column.key, 254 | rowIndex, 255 | cellIndex, 256 | paintText: textLine, 257 | fixed: column.fixed === true, 258 | readOnly: column.readOnly === true, 259 | buttons, 260 | renderText: column.renderText, 261 | renderButton: column.renderButton, 262 | rowData: item, 263 | type: column.type, 264 | }) 265 | } 266 | cellIndex += 1 267 | } 268 | allCells.push(cellTemp) 269 | 270 | let showDot = false 271 | if (this.showDot) { 272 | if (item[this.showDot.key] === this.showDot.value) { 273 | showDot = true 274 | } 275 | } 276 | allRows.push({ 277 | height: maxHeight, 278 | rowIndex, 279 | showDot, 280 | }) 281 | rowIndex += 1 282 | } 283 | for (const item of fixedColumns) { 284 | const temp = [] 285 | let index = 0 286 | for (const row of allCells) { 287 | const cell = row[item.cellIndex] 288 | temp.push({ 289 | ...cell, 290 | height: allRows[index].height, 291 | }) 292 | index += 1 293 | } 294 | allFixedCells.push(temp) 295 | } 296 | }, 297 | setAllCells(startIndex) { 298 | const { rowHeight, ctx, getTextLine, allRows, allCells, columns } = this 299 | let rowIndex = startIndex 300 | for (let i = startIndex; i < this.data.length; i += 1) { 301 | const item = this.data[i] 302 | let maxHeight = rowHeight 303 | let cellIndex = 0 304 | const cellTemp = [] 305 | for (const column of columns) { 306 | let text = '' 307 | let buttons 308 | let textLine 309 | if (column.renderText) { 310 | text = column.renderText(item) 311 | } else if (column.renderButton) { 312 | buttons = column.renderButton(this.data[rowIndex], rowIndex) 313 | } else { 314 | text = item[column.key] 315 | } 316 | if (text || text === 0) { 317 | textLine = getTextLine(ctx, text, column.width ? column.width : 100) 318 | let textLineCount = 0 319 | if (textLine) { 320 | textLineCount = textLine.length 321 | } 322 | if (textLineCount > 1) { 323 | if (maxHeight < rowHeight + ((textLineCount - 1) * 18)) { 324 | maxHeight = rowHeight + ((textLineCount - 1) * 18) 325 | } 326 | } 327 | } 328 | cellTemp.push({ 329 | width: column.width ? column.width : 100, 330 | content: item[column.key], 331 | key: column.key, 332 | rowIndex, 333 | cellIndex, 334 | paintText: textLine, 335 | fixed: column.fixed === true, 336 | readOnly: column.readOnly === true, 337 | buttons, 338 | renderText: column.renderText, 339 | renderButton: column.renderButton, 340 | rowData: item, 341 | type: column.type, 342 | }) 343 | cellIndex += 1 344 | } 345 | allCells.push(cellTemp) 346 | 347 | let showDot = false 348 | if (this.showDot) { 349 | if (item[this.showDot.key] === this.showDot.value) { 350 | showDot = true 351 | } 352 | } 353 | allRows.push({ 354 | height: maxHeight, 355 | rowIndex, 356 | showDot, 357 | }) 358 | rowIndex += 1 359 | } 360 | this.setBodyHeight(this.allRows, this.originPoint) 361 | this.resetScrollBar(this.maxPoint, this.bodyWidth, this.bodyHeight, this.fixedWidth) 362 | }, 363 | initRowHeight() { 364 | 365 | }, 366 | setCellItem(rowIndex, cellIndex, text) { 367 | const { ctx, allRows, allCells, getTextLine, rowHeight } = this 368 | const row = allRows[rowIndex] 369 | const cell = allCells[rowIndex][cellIndex] 370 | let maxHeight = 0 371 | const textLine = getTextLine(ctx, text, cell.width) 372 | let textLineCount = 0 373 | if (textLine) { 374 | textLineCount = textLine.length 375 | } 376 | if (textLineCount > 1) { 377 | if (maxHeight < rowHeight + ((textLineCount - 1) * 18)) { 378 | maxHeight = rowHeight + ((textLineCount - 1) * 18) 379 | } 380 | } 381 | if (maxHeight > row.height) { 382 | row.height = maxHeight 383 | } 384 | cell.content = text 385 | cell.paintText = textLine 386 | }, 387 | setCellItemByKey(rowIndex, key, text) { 388 | const { ctx, allRows, allCells, getTextLine, rowHeight } = this 389 | const row = allRows[rowIndex] 390 | const cells = allCells[rowIndex] 391 | let index = 0 392 | let cell = null 393 | for (const item of cells) { 394 | if (item.key === key) { 395 | cell = allCells[rowIndex][index] 396 | break 397 | } 398 | index += 1 399 | } 400 | if (cell) { 401 | let maxHeight = 0 402 | const textLine = getTextLine(ctx, text, cell.width) 403 | let textLineCount = 0 404 | if (textLine) { 405 | textLineCount = textLine.length 406 | } 407 | if (textLineCount > 1) { 408 | if (maxHeight < rowHeight + ((textLineCount - 1) * 18)) { 409 | maxHeight = rowHeight + ((textLineCount - 1) * 18) 410 | } 411 | } 412 | if (maxHeight > row.height) { 413 | row.height = maxHeight 414 | this.initSize() 415 | } 416 | cell.content = text 417 | cell.paintText = textLine 418 | } 419 | }, 420 | setCellItemAll(rowIndex, data) { 421 | let index = 0 422 | for (const item of data) { 423 | this.setCellItem(rowIndex, index, item) 424 | index += 1 425 | } 426 | }, 427 | getDisplayCells(displayRows, displayColumns) { 428 | const temp = [] 429 | const { allCells, fillWidth, setCellRenderText } = this 430 | for (const row of displayRows) { 431 | const cellTemp = [] 432 | for (const column of displayColumns) { 433 | let cell = allCells[row.rowIndex][column.cellIndex] 434 | if (cell.renderText) { 435 | cell = setCellRenderText(cell) 436 | } 437 | if (cell.renderButton) { 438 | cell.buttons = column.renderButton(this.data[cell.rowIndex], cell.rowIndex) 439 | } 440 | const cellClone = Object.assign({}, cell, { x: column.x, y: row.y, width: cell.width + fillWidth, height: row.height }) //eslint-disable-line 441 | cellTemp.push(cellClone) 442 | } 443 | temp.push(cellTemp) 444 | } 445 | setTimeout(() => { this.displayCells = [...temp] }, 0) 446 | return temp 447 | }, 448 | setCellRenderText(cell) { 449 | const text = cell.renderText(cell.rowData) 450 | const row = this.allRows[cell.rowIndex] 451 | if (text) { 452 | let maxHeight = 0 453 | const textLine = this.getTextLine(this.ctx, text, cell.width) 454 | let textLineCount = 0 455 | if (textLine) { 456 | textLineCount = textLine.length 457 | } 458 | if (textLineCount > 1) { 459 | if (maxHeight < this.rowHeight + ((textLineCount - 1) * 18)) { 460 | maxHeight = this.rowHeight + ((textLineCount - 1) * 18) 461 | } 462 | } 463 | if (maxHeight > row.height) { 464 | row.height = maxHeight 465 | } 466 | cell.content = text 467 | cell.paintText = textLine 468 | } else { 469 | cell.content = '' 470 | cell.paintText = [] 471 | } 472 | return cell 473 | }, 474 | getDisplayFixedCells(displayRows) { 475 | const temp = [] 476 | const { allFixedCells, fillWidth } = this 477 | for (const fixedCell of allFixedCells) { 478 | const fixedCellTemp = [] 479 | for (const row of displayRows) { 480 | const fixed = fixedCell[row.rowIndex] 481 | if (fixed.renderButton) { 482 | fixed.buttons = fixed.renderButton(this.data[fixed.rowIndex], fixed.rowIndex) 483 | } 484 | const fixedCellClone = Object.assign({}, fixed, { y: row.y, width: fixed.width + fillWidth, height: row.height }) 485 | fixedCellTemp.push(fixedCellClone) 486 | } 487 | temp.push(fixedCellTemp) 488 | } 489 | setTimeout(() => { this.displayallFixedCells = [...temp] }, 0) 490 | return temp 491 | }, 492 | getDisplayRows() { 493 | const { offset: { y }, originPoint, maxPoint, allRows } = this 494 | const temp = [] 495 | let startY = originPoint.y + y 496 | for (const row of allRows) { 497 | if (startY + row.height > originPoint.y 498 | && startY < maxPoint.y) { 499 | const rowClone = Object.assign({}, row, { y: startY }) 500 | temp.push(rowClone) 501 | } else if (startY >= maxPoint.y) { 502 | break 503 | } 504 | startY += row.height 505 | } 506 | setTimeout(() => { this.displayRows = [...temp] }, 0) 507 | return temp 508 | }, 509 | getDisplayColumns() { 510 | const { offset: { x }, originPoint, maxPoint, allColumns, fillWidth } = this 511 | const temp = [] 512 | let startX = originPoint.x + x 513 | 514 | for (const column of allColumns) { 515 | if (column.checked) { 516 | const width = column.width + fillWidth 517 | if (width + startX > originPoint.x && startX < maxPoint.x) { 518 | const columnClone = Object.assign({}, column, { x: startX, width }) 519 | temp.push(columnClone) 520 | } 521 | startX += width 522 | } 523 | } 524 | setTimeout(() => { this.displayColumns = [...temp] }, 0) 525 | return temp 526 | }, 527 | getTextLine(ctx, text, width) { 528 | if (!text && text !== 0) { 529 | return null 530 | } 531 | const chr = `${text}`.split('') 532 | let temp = '' 533 | const row = [] 534 | for (let a = 0; a < chr.length; a += 1) { 535 | if (ctx.measureText(temp).width >= width - 20) { 536 | row.push(temp) 537 | temp = '' 538 | } 539 | temp += chr[a] 540 | } 541 | row.push(temp) 542 | return row 543 | }, 544 | getCellAt(x, y) { 545 | for (const rows of this.displayCells) { 546 | for (const cell of rows) { 547 | if (x >= cell.x && y >= cell.y && x <= cell.x + cell.width && y <= cell.y + cell.height) { 548 | return Object.assign({}, cell, { offset: { ...this.offset } }) 549 | } 550 | } 551 | } 552 | return null 553 | }, 554 | getCheckboxAt(x, y) { 555 | for (const check of this.checkboxs) { 556 | if (x >= check.x && y >= check.y && x <= check.x + check.width && y <= check.y + check.height) { 557 | return Object.assign({}, check) 558 | } 559 | } 560 | return null 561 | }, 562 | getButtonAt(x, y) { 563 | for (const button of this.renderButtons) { 564 | if (x >= button.x && y >= button.y && x <= button.x + button.width && y <= button.y + button.height) { 565 | return Object.assign({}, button) 566 | } 567 | } 568 | return null 569 | }, 570 | getCellsBySelect(area) { 571 | const cells = [] 572 | for (let i = area.rowIndex; i < area.rowIndex + area.rowCount; i += 1) { 573 | const row = this.allCells[i] 574 | const temp = [] 575 | let startX = 0 576 | let maxWidth = Infinity 577 | for (let j = 0; j < row.length; j += 1) { 578 | if (area.cellIndex === j) { 579 | maxWidth = startX + area.width 580 | } 581 | if (startX < maxWidth && j >= area.cellIndex) { 582 | temp.push(row[j]) 583 | } else if (startX > maxWidth) { 584 | break 585 | } 586 | startX += row[j].width + this.fillWidth 587 | } 588 | cells.push(temp) 589 | } 590 | return cells 591 | }, 592 | getCellByRowAndKey(rowIndex, key) { 593 | const cells = this.allCells[rowIndex] 594 | for (const cell of cells) { 595 | if (cell.key === key) { 596 | return cell 597 | } 598 | } 599 | return null 600 | }, 601 | focusCellByOriginCell(cell) { 602 | for (const row of this.displayCells) { 603 | for (const item of row) { 604 | if (item.rowIndex === cell.rowIndex && item.key === cell.key) { 605 | const focusCell = Object.assign({}, item, { offset: { ...this.offset } }) 606 | this.focusCell = focusCell 607 | this.rowFocus = { 608 | cellX: focusCell.x, 609 | cellY: focusCell.y, 610 | rowIndex: this.focusCell.rowIndex, 611 | offset: { ...this.offset }, 612 | } 613 | this.paintFocusCell(focusCell) 614 | return focusCell 615 | } 616 | } 617 | } 618 | return null 619 | }, 620 | freshFocusCell(rowIndex, cellIndex, displayRows, displayColumns) { 621 | const firstRowIndex = displayRows[0].rowIndex 622 | const lastRowIndex = displayRows[displayRows.length - 1].rowIndex 623 | if (rowIndex >= firstRowIndex && rowIndex <= lastRowIndex) { 624 | this.focusCell.height = displayRows[rowIndex - firstRowIndex].height 625 | } 626 | for (const item of displayColumns) { 627 | if (item.cellIndex === cellIndex) { 628 | this.focusCell.width = item.width 629 | } 630 | } 631 | }, 632 | initDisplayItems() { 633 | const displayColumns = this.getDisplayColumns() 634 | const displayRows = this.getDisplayRows() 635 | const displayCells = this.getDisplayCells(displayRows, displayColumns) 636 | const displayFixedCells = this.getDisplayFixedCells(displayRows) 637 | if (this.focusCell) { 638 | this.freshFocusCell(this.focusCell.rowIndex, this.focusCell.cellIndex, displayRows, displayColumns) 639 | const lastOffset = this.focusCell.offset 640 | if (lastOffset.x !== this.offset.x || lastOffset.y !== this.offset.y) { 641 | this.focusCell.x -= lastOffset.x - this.offset.x 642 | this.focusCell.y -= lastOffset.y - this.offset.y 643 | this.focusCell.offset = { ...this.offset } 644 | } 645 | } 646 | if (this.selectArea) { 647 | const lastOffset = this.selectArea.offset 648 | this.selectArea.x -= lastOffset.x - this.offset.x 649 | this.selectArea.y -= lastOffset.y - this.offset.y 650 | this.selectArea.offset = { ...this.offset } 651 | let height = 0 652 | for (let i = this.selectArea.rowIndex; i < (this.selectArea.rowIndex + this.selectArea.rowCount); i += 1) { 653 | height += this.allRows[i].height 654 | } 655 | this.selectArea.height = height 656 | } 657 | if (this.rowFocus) { 658 | const lastOffset = this.rowFocus.offset 659 | if (lastOffset.x !== this.offset.x || lastOffset.y !== this.offset.y) { 660 | this.rowFocus.y -= lastOffset.y - this.offset.y 661 | this.rowFocus.cellY -= lastOffset.y - this.offset.y 662 | if (!this.rowFocus.fixed) { 663 | this.rowFocus.x -= lastOffset.x - this.offset.x 664 | this.rowFocus.cellX -= lastOffset.x - this.offset.x 665 | } 666 | this.rowFocus.offset = { ...this.offset } 667 | } 668 | } 669 | return { displayColumns, displayRows, displayCells, displayFixedCells } 670 | }, 671 | }, 672 | } 673 | --------------------------------------------------------------------------------