├── .gitignore ├── entry.js ├── src ├── demo.gif ├── demo.html ├── tableEditorDemo.js └── tableEditor.js ├── webpack.config.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /entry.js: -------------------------------------------------------------------------------- 1 | import init from "./src/tableEditorDemo"; 2 | 3 | init() -------------------------------------------------------------------------------- /src/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevefoxuser/html-table-editor-resizer/HEAD/src/demo.gif -------------------------------------------------------------------------------- /src/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tableEditor demo 6 | 7 | 8 | 9 |
10 | rows: 11 | columns: 12 | 13 | 14 |
15 |
 
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/tableEditorDemo.js: -------------------------------------------------------------------------------- 1 | import TableEditor from "./tableEditor" 2 | function init () { 3 | const $ = function (str) { 4 | return str[0] === '#' ? document.querySelector(str) : document.querySelectorAll(str) 5 | } 6 | const t = new TableEditor() 7 | $('#btn').addEventListener('click', function () { 8 | var rows = parseInt($('#rows').value) 9 | var columns = parseInt($('#columns').value) 10 | t.Create(rows, columns, $('#container')) 11 | }) 12 | 13 | $('#btn2').addEventListener('click', function () { 14 | alert(t.GetTableStr()) 15 | }) 16 | 17 | $('#btn').click() 18 | } 19 | 20 | 21 | export default init -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './entry.js', 6 | output: { 7 | filename: 'main.js', 8 | path: path.resolve(__dirname, 'dist') 9 | }, 10 | plugins: [ 11 | new HtmlWebpackPlugin({ 12 | title: '', 13 | template: './src/demo.html', 14 | filename: 'index.html', 15 | 16 | }) 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /(node_modules|bower_components)/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: ['@babel/preset-env'] 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | 33 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 原生javascript表格插件,可以拖动改变大小,合并单元格等 2 | 3 | > Demo 4 | 5 | 6 | 7 | > 安装 8 | 9 | ### npm install 10 | 11 | ### npm run dev 12 | 13 | ### 查看 localhost:8080 14 | 15 | > 16 | ### 三种方式 初始化表格 17 | ```javascript 18 | import TableEditor from "./tableEditor" 19 | // better to replace document.body with your own div container 20 | // 最好把body换成某个空的div容器,这个div不要放其他元素 21 | const t = new TableEditor() 22 | 23 | t.Create(10, 10, document.body) 24 | 25 | // or 26 | t.CreateFromString('
',document.body) 27 | 28 | // or 29 | t.CreateFromElem(document.querySelector('#YourTable'),document.body) 30 | 31 | // do some edit ..... 32 | 33 | // 编辑完表格以后获取编辑后的结果 34 | // get the edited table 35 | console.log(t.GetTable()) 36 | console.log(t.GetTableStr()) 37 | ``` 38 | 39 | 卧槽,放了这么久了,真就一个屎大都没有啊 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "table_editor", 3 | "version": "1.0.0", 4 | "description": "Design html table by clicking and draging", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "dev": "webpack-dev-server --mode development" 9 | }, 10 | "dependencies": {}, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stevefoxuser/tableEditor.git" 14 | }, 15 | "keywords": [ 16 | "table", 17 | "editor", 18 | "htmltable" 19 | ], 20 | "author": "chenyouming", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/stevefoxuser/tableEditor/issues" 24 | }, 25 | "homepage": "https://github.com/stevefoxuser/tableEditor#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.9.0", 28 | "@babel/preset-env": "^7.9.5", 29 | "babel-loader": "^8.1.0", 30 | "babel-preset-env": "^1.7.0", 31 | "expose-loader": "^0.7.5", 32 | "html-webpack-plugin": "^4.2.0", 33 | "webpack": "^4.43.0", 34 | "webpack-cli": "^3.3.11", 35 | "webpack-dev-server": "^3.10.3" 36 | } 37 | } -------------------------------------------------------------------------------- /src/tableEditor.js: -------------------------------------------------------------------------------- 1 | // config 2 | const conf = { 3 | language: 'cn', 4 | cursorWidth: 3, 5 | minWidth: 20, 6 | minHeight: 20 7 | } 8 | const d = document 9 | const $ = function (str, findIn) { 10 | return str[0] === '#' ? (findIn || d).querySelector(str) : (findIn || d).querySelectorAll(str) 11 | } 12 | const getPos = function (el, prop) { 13 | let result = el[prop] 14 | while (el.offsetParent && el.tagName !== 'body') { 15 | el = el.offsetParent 16 | result += el[prop] 17 | } 18 | return result 19 | } 20 | const isParent = function (el, checkp) { 21 | let result = el === checkp 22 | while (el.parentNode && el.parentNode.tagName !== 'body') { 23 | el = el.parentNode 24 | if (el === checkp) { 25 | result = true 26 | } 27 | } 28 | return result 29 | } 30 | const getPageX = function (el) { 31 | return getPos(el, 'offsetLeft') 32 | } 33 | const getPageY = function (el) { 34 | return getPos(el, 'offsetTop') 35 | } 36 | const hasClass = function (el, className) { 37 | return el.className ? el.className.split(/\s+/g).includes(className) : false 38 | } 39 | const addClass = function (el, className) { 40 | if (hasClass(el, className)) return 41 | let _class = el.className + ' ' + className 42 | _class = _class.replace(/^\s+/, '').replace(/\s+$/, '') 43 | el.className = _class 44 | } 45 | const removeClass = function (el, className) { 46 | if (!el.className) return 47 | let newClassName = el.className.replace(className, '') 48 | newClassName = newClassName.replace(/^\s+/, '').replace(/\s+$/, '') 49 | el.className = newClassName.split(/\s+/g).join(' ') 50 | if (/^\s+$/.test(el.className) || !el.className) { 51 | el.removeAttribute('class') 52 | } 53 | } 54 | const getIndex = function (el) { 55 | let index = null 56 | const tg = el.tagName.toLowerCase() 57 | $(tg, el.parentNode).forEach((_e, k) => { 58 | if (_e === el) { 59 | index = k 60 | } 61 | }) 62 | return index 63 | } 64 | const insertAfter = function (newNode, existingNode) { 65 | const parent = existingNode.parentNode; 66 | if (parent.lastChild === existingNode) { 67 | parent.appendChild(newNode) 68 | } else { 69 | parent.insertBefore(newNode, existingNode.nextSibling) 70 | } 71 | } 72 | const checkIfOutOfScreen = function (popoverEl, top) { 73 | if (top + popoverEl.offsetHeight > d.documentElement.clientHeight) { 74 | popoverEl.style.top = d.documentElement.clientHeight - popoverEl.offsetHeight + 'px' 75 | } 76 | } 77 | const getRowSpan = function (el) { 78 | return el.getAttribute('rowspan') ? parseInt(el.getAttribute('rowspan')) : 1 79 | } 80 | const getColSpan = function (el) { 81 | return el.getAttribute('colspan') ? parseInt(el.getAttribute('colspan')) : 1 82 | } 83 | 84 | class TableEditor { 85 | constructor () { 86 | this.el = null 87 | this.pNode = null 88 | this.action = null 89 | this.actd = null 90 | this.selectDirection = null 91 | this.menuState = 'hidden' 92 | this.ms = 'up' 93 | this.startY = null 94 | this.startHeight = null 95 | this.startX = null 96 | this.startWidth = null 97 | this.readonly = false 98 | // this.freezeMove = false 99 | 100 | // 拖动光标生效范围必须比td最小宽度要小,否则会产生很多额外的逻辑判断当前拖动的是哪一个td,没有实用价值 101 | if (conf.cursorWidth >= conf.minWidth) conf.minWidth = conf.cursorWidth + 1 102 | if (conf.cursorWidth >= conf.minHeight) conf.minHeight = conf.cursorWidth + 1 103 | // 表格默认的样式,所有实例只创建一个样式标签 104 | if (!$('#table_editor_global')) { 105 | const style = d.createElement('style') 106 | style.setAttribute('id', 'table_editor_global') 107 | style.setAttribute('type', 'text/css') 108 | style.innerHTML = ` 109 | #new_context_menu{ 110 | position:absolute; 111 | z-index:999999999; 112 | } 113 | #new_context_menu ul{ 114 | list-style-type:none; 115 | margin:0; 116 | padding:0; 117 | border-top:1px solid #ccc; 118 | border-right:1px solid #ccc; 119 | } 120 | #new_context_menu ul li{ 121 | background:#fff; 122 | padding:10px; 123 | border-bottom:1px solid #ccc; 124 | border-left:1px solid #ccc; 125 | cursor:pointer; 126 | clear:both; 127 | overflow:hidden; 128 | } 129 | #new_context_menu ul li:hover{ 130 | background:#eee; 131 | } 132 | #new_context_menu ul li.disabled{ 133 | color:#999; 134 | cursor:default; 135 | background:#fff; 136 | } 137 | #new_context_menu ul li.disabled:hover{ 138 | color:#999; 139 | cursor:default; 140 | background:#fff; 141 | } 142 | .namespace_table_editor td{ 143 | vertical-align:top; 144 | padding:2px 2px 2px 2px; 145 | text-align:center; 146 | vertical-align:middle; 147 | font-size:12px; 148 | } 149 | .namespace_table_editor .selected{ 150 | background:#ccc; 151 | } 152 | 153 | #namespace_style_ul{ 154 | width:210px; 155 | } 156 | .namespace_left_div{ 157 | width:100px; 158 | float:left; 159 | } 160 | .namespace_right_div input,.namespace_right_div select{ 161 | color:#333; 162 | } 163 | .namespace_right_div{ 164 | width:80px; 165 | float:left; 166 | } 167 | .namespace_right_div input{ 168 | width:40px; 169 | } 170 | ` 171 | d.body.appendChild(style) 172 | } 173 | this.menuList = { 174 | cn: { 175 | insertRowAfter: '在下方插入行', 176 | insertColumnAfter: '在末尾插入列', 177 | deleteRow: '删除行', 178 | deleteColumn: '删除列', 179 | mergeCells: '合并单元格', 180 | splitCell: '拆分单元格', 181 | cellStyle: '单元格样式', 182 | }, 183 | en: { 184 | insertRowAfter: 'insert row after', 185 | insertColumnAfter: 'insert column after', 186 | deleteRow: 'delete selected row', 187 | deleteColumn: 'delete selected column', 188 | mergeCells: 'merge cells', 189 | splitCell: 'split cell', 190 | cellStyle: 'edit cell style', 191 | } 192 | }[conf.language || 'cn'] 193 | 194 | this.styleList = { 195 | cn: { 196 | verticalAlign: '垂直对齐', 197 | textAlign: '水平对齐', 198 | fontSize: '字体大小', 199 | lineHeight: '行高', 200 | height: '高度', 201 | width: '宽度', 202 | paddingTop: '上边距', 203 | paddingRight: '右边距', 204 | paddingBottom: '下边距', 205 | paddingLeft: '左边距', 206 | }, 207 | en: { 208 | verticalAlign: 'vertical align', 209 | textAlign: 'text align', 210 | fontSize: 'font size', 211 | lineHeight: 'line height', 212 | height: 'height', 213 | width: 'width', 214 | paddingTop: 'padding top', 215 | paddingRight: 'padding right', 216 | paddingBottom: 'padding bottom', 217 | paddingLeft: 'padding left', 218 | } 219 | }[conf.language || 'cn'] 220 | } 221 | Create (rows, columns, appendTo) { 222 | let tableStr = [] 223 | for (let i = 0; i < rows; i++) { 224 | let _tmp = '' 225 | for (let j = 0; j < columns; j++) { 226 | _tmp += `` 227 | } 228 | _tmp += '' 229 | tableStr.push(_tmp) 230 | } 231 | // const id = this.newId() 232 | let result = `${tableStr.join('')}
` 233 | if (appendTo) { 234 | appendTo.innerHTML = result 235 | this.initTable($('table', appendTo)[0]) 236 | } 237 | return result 238 | } 239 | CreateFromString (str, appendTo) { 240 | appendTo.innerHTML = str 241 | this.initTable($('table', appendTo)[0]) 242 | } 243 | CreateFromElem (elem) { 244 | this.pNode = elem.parentNode 245 | this.initTable(elem) 246 | } 247 | initTable (elem) { 248 | this.el = elem 249 | addClass(this.el, 'namespace_table_editor') 250 | this.el.ondragstart = function () { 251 | return false 252 | } 253 | this.el.style.borderCollapse = 'collapse' 254 | this.el.style.wordBreak = 'break-all' 255 | // if (styleMap) { 256 | // Object.keys(styleMap).forEach(k => { 257 | // this.el.style[k] = styleMap[k] 258 | // }) 259 | // } 260 | // if (!this.readonly) this.el.setAttribute('contenteditable', true) 261 | this.bindEvents() 262 | } 263 | setReadOnly () { 264 | this.readonly = true 265 | this.el.style.cursor = 'default' 266 | this.el.setAttribute('contenteditable', false) 267 | } 268 | setWriteAble () { 269 | this.readonly = false 270 | this.el.setAttribute('contenteditable', true) 271 | } 272 | GetTableStr () { 273 | // if (directly) { 274 | return this.el.parentNode.innerHTML 275 | // } else { 276 | const elem = this.el.cloneNode(true) 277 | elem.style.cursor = 'default' 278 | // elem.setAttribute('contenteditable', false) 279 | let div = d.createElement('div') 280 | div.appendChild(elem) 281 | setTimeout(() => { 282 | div = null 283 | }) 284 | return div.innerHTML 285 | // } 286 | } 287 | GetTable () { 288 | const elem = this.el.cloneNode(true) 289 | elem.style.cursor = 'default' 290 | // elem.setAttribute('contenteditable', false) 291 | return elem 292 | } 293 | bindData (dataSourceListStr) { 294 | this.el.setAttribute('data-source', dataSourceListStr) 295 | } 296 | getDataSource () { 297 | return this.el.getAttribute('data-source') 298 | } 299 | cellFilter (x, y) { 300 | const result = [] 301 | if (typeof x !== 'function') { 302 | if (x === -1) x = [-Infinity, Infinity] 303 | if (y === -1) y = [-Infinity, Infinity] 304 | if (typeof x === 'number') x = [x, x] 305 | if (typeof y === 'number') y = [y, y] 306 | if (x[0] > x[1]) x.reverse() 307 | if (y[0] > y[1]) y.reverse() 308 | $('td', this.el).forEach(td => { 309 | if (td.offsetLeft >= x[0] && td.offsetLeft <= x[1] && td.offsetTop >= y[0] && td.offsetTop <= y[1]) { 310 | result.push(td) 311 | } 312 | }) 313 | } else { 314 | $('td', this.el).forEach(td => { 315 | if (x(td)) { 316 | result.push(td) 317 | } 318 | }) 319 | } 320 | return result 321 | } 322 | newId () { 323 | return `table_editor_${$('.namespace_table_editor').length}` 324 | } 325 | bindEvents () { 326 | this.el.addEventListener('mousedown', (e) => { 327 | // if (!this.freezeMove) return console.log('freeze') 328 | if (e.buttons === 1) { 329 | this.downEvent(e) 330 | } 331 | }) 332 | d.addEventListener('mouseup', (e) => { 333 | // this.freezeMove = true 334 | // setTimeout(() => { 335 | // this.freezeMove = false 336 | // }, 300) 337 | this.upEvent(e) 338 | }) 339 | d.addEventListener('mousemove', (e) => { 340 | // if (!this.freezeMove) return console.log('freeze') 341 | this.moveEvent(e) 342 | }) 343 | this.el.addEventListener('contextmenu', (e) => { 344 | this.newContextMenu(e) 345 | }) 346 | this.el.addEventListener('dblclick', (e) => { 347 | this.dbclickEvents(e) 348 | }) 349 | } 350 | dbclickEvents (e) { 351 | let td 352 | let oldtxt 353 | let textarea 354 | if (e.target.tagName === 'TD') { 355 | td = e.target 356 | textarea = td.querySelector('textarea') || d.createElement('textarea') 357 | oldtxt = td.innerText 358 | } else if (e.target.tagName === 'textarea') { 359 | td = e.target.parentNode 360 | textarea = e.target 361 | oldtxt = textarea.value 362 | } else { 363 | return 364 | } 365 | textarea.style.height = td.offsetHeight - 1 + 'px' 366 | textarea.style.width = '100%' 367 | textarea.style.overflow = 'hidden' 368 | textarea.innerText = oldtxt 369 | textarea.style.background = '#fff' 370 | textarea.style.resize = 'none' 371 | textarea.style.border = 'none' 372 | textarea.style.display = 'block' 373 | td.innerHTML = '' 374 | td.appendChild(textarea) 375 | textarea.select() 376 | textarea.addEventListener('blur', () => { 377 | td.innerText = textarea.value 378 | }) 379 | } 380 | downEvent (e) { 381 | // e.preventDefault() 382 | this.ms = 'down' 383 | if (e.target.tagName !== 'TD') return 384 | const td = e.target 385 | this.actd = null 386 | 387 | // if (['col-resize', 'row-resize', 'select'].includes(this.action)) { 388 | this.clearSelect() 389 | if (this.action === 'select') { 390 | this.actd = td 391 | } else if (this.action === 'col-resize') { 392 | if (e.offsetX >= td.offsetWidth - conf.cursorWidth) { 393 | this.actd = td 394 | } else if (e.offsetX <= conf.cursorWidth) { 395 | // 鼠标在cell左侧的时候计算要拖动的cell是哪个,默认应该是前一个td,但是有rowspan的干扰,所以要重新计算 396 | const tds = this.cellFilter(_cell => { 397 | if (_cell.offsetLeft + _cell.offsetWidth + conf.minWidth >= td.offsetLeft 398 | && _cell.offsetLeft + _cell.offsetWidth < td.offsetLeft + td.offsetWidth) return true 399 | }) 400 | this.actd = tds[tds.length - 1] 401 | } 402 | this.startX = e.pageX 403 | this.startWidth = this.actd.offsetWidth 404 | } else if (this.action === 'row-resize') { 405 | if (e.offsetY >= td.offsetHeight - conf.cursorWidth) { 406 | this.actd = td 407 | } else if (e.offsetY <= conf.cursorWidth) { 408 | const tds = this.cellFilter(_cell => { 409 | if (_cell.offsetTop + _cell.offsetHeight + conf.minWidth >= td.offsetTop 410 | && _cell.offsetTop + _cell.offsetHeight < td.offsetTop + td.offsetHeight) return true 411 | }) 412 | this.actd = tds[tds.length - 1] 413 | } 414 | this.startY = e.pageY 415 | this.startHeight = this.actd.offsetHeight 416 | } 417 | else { 418 | console.log('No action triggered.') 419 | } 420 | } 421 | upEvent (e) { 422 | // e.preventDefault() 423 | this.ms = 'up' 424 | this.actd = null 425 | this.el.style.cursor = 'default' 426 | // 选中td 427 | if (this.selectDirection && this.menuState === 'hidden') { 428 | // this.selectDirection = null 429 | this.newContextMenu(e, 'select') 430 | } 431 | } 432 | cancelResize (msg) { 433 | this.actd = null 434 | this.action = null 435 | this.ms = 'up' 436 | this.el.style.cursor = 'default' 437 | if (msg) alert(msg) 438 | } 439 | setAction (action) { 440 | if (action) { 441 | this.el.style.cursor = action 442 | this.action = action 443 | } else { 444 | this.el.style.cursor = 'default' 445 | this.action = null 446 | } 447 | } 448 | moveEvent (e) { 449 | if (this.ms === 'down') { 450 | if (isParent(e.target, this.el)) { 451 | e.preventDefault() 452 | } 453 | if (this.action === 'col-resize') { 454 | this.colResize(e) 455 | } else if (this.action === 'row-resize') { 456 | this.rowResize(e) 457 | } else if (this.action === 'select') { 458 | this.select(e) 459 | } else { 460 | console.log('No action triggered.') 461 | } 462 | return 463 | } 464 | const td = e.target 465 | this.setAction() 466 | if (isParent(e.target, this.el)) { 467 | e.preventDefault() 468 | } else { 469 | return 470 | } 471 | if (e.offsetX >= td.offsetWidth - conf.cursorWidth) { 472 | /** colspan大于1的时候从右侧border拖动改变宽度时显示有点bug,要避免这个问题计算繁琐,直接避过。 473 | * 可以把parseInt(td.getAttribute('colspan')) <= 1这个限制条件去掉,在colspan大于1的单元格,从内侧拖动一下试试 **/ 474 | if (td.offsetLeft !== 0 && getColSpan(td) <= 1) { 475 | this.setAction('col-resize') 476 | } 477 | } else if (e.offsetX <= conf.cursorWidth) { 478 | // 同上面colspan计算的问题,一样的问题,一样的处理 479 | if (td.offsetLeft !== 0 && getColSpan(td) <= 1) { 480 | this.setAction('col-resize') 481 | } 482 | } else if (e.offsetY >= td.offsetHeight - conf.cursorWidth) { 483 | // 同上面colspan计算的问题,一样的问题,一样的处理 484 | if (td.offsetTop !== 0 && getRowSpan(td) <= 1) { 485 | this.setAction('row-resize') 486 | } 487 | } else if (e.offsetY <= conf.cursorWidth) { 488 | if (td.offsetTop !== 0 && getRowSpan(td) <= 1) { 489 | this.setAction('row-resize') 490 | } 491 | } else if (isParent(e.target, this.el)) { 492 | this.action = 'select' 493 | this.el.style.cursor = 'default' 494 | } 495 | } 496 | colResize (e) { 497 | if (!this.actd) return 498 | let offset = e.pageX - this.startX 499 | const w = offset + this.startWidth 500 | const tds = this.cellFilter(this.actd.offsetLeft, -1) 501 | tds.forEach(td => { 502 | td.style.width = (w > conf.minWidth ? w : conf.minWidth) + 'px' 503 | }) 504 | } 505 | rowResize (e) { 506 | if (!this.actd) return 507 | // const h = e.y - getPageY(this.actd) 508 | let offset = e.pageY - this.startY 509 | const h = offset + this.startHeight 510 | const tds = this.cellFilter(-1, this.actd.offsetTop) 511 | tds.forEach(td => { 512 | td.style.height = (h > conf.minHeight ? h : conf.minHeight) + 'px' 513 | }) 514 | // const tds = this.cellFilter(this.actd.) 515 | // this.actd.style.height = (h > conf.minHeight ? h : conf.minHeight) + 'px' 516 | } 517 | select (e) { 518 | // addClass(this.actd, 'selected') 519 | const td = e.target 520 | if (td === this.actd) return 521 | if (!isParent(td, this.el)) return 522 | if (!this.actd) return 523 | let sx = this.actd.offsetLeft, sy = this.actd.offsetTop 524 | let cx = td.offsetLeft, cy = td.offsetTop 525 | if (this.selectDirection) { 526 | $('td', this.el).forEach(_td => { 527 | removeClass(_td, 'selected') 528 | }) 529 | let getTds = [] 530 | if (this.selectDirection === 'y' && sx === cx) { 531 | getTds = this.cellFilter(sx, [sy, cy]) 532 | } else if (this.selectDirection === 'x' && sy === cy) { 533 | getTds = this.cellFilter([sx, cx], sy) 534 | } 535 | // check colspan again 536 | if (getTds.length) { 537 | let cols = getColSpan(getTds[0]), rows = getRowSpan(getTds[0]), warn = false 538 | getTds.forEach(_td => { 539 | if (cols !== getColSpan(_td) || rows !== getRowSpan(_td)) { 540 | return warn = true 541 | } 542 | addClass(_td, 'selected') 543 | }) 544 | if (warn) { 545 | getTds.forEach(_td => removeClass(_td, 'selected')) 546 | console.warn('只有colspan和rowspan一样的单元格可以合并') 547 | } 548 | } 549 | } else { 550 | if (getColSpan(td) !== getColSpan(this.actd) || 551 | getRowSpan(td) !== getRowSpan(this.actd)) { 552 | console.warn('只有colspan和rowspan一样的单元格可以合并') 553 | return 554 | } 555 | if (sx === cx) { 556 | this.pushSelect(td, 'y') 557 | } else if (td === this.actd.previousSibling || td === this.actd.nextSibling) { 558 | this.pushSelect(td, 'x') 559 | } 560 | } 561 | } 562 | pushSelect (td, d) { 563 | addClass(td, 'selected') 564 | addClass(this.actd, 'selected') 565 | this.selectDirection = d 566 | } 567 | clearSelect () { 568 | const tds = this.el.querySelectorAll('.selected') 569 | tds.forEach(td => removeClass(td, 'selected')) 570 | this.selectDirection = null 571 | } 572 | newContextMenu (e, triggerBy) { 573 | e.preventDefault() 574 | let banMenu = [] 575 | if (triggerBy === 'select') { 576 | let tds = $('.selected', this.el) 577 | if (!tds.length) return 578 | banMenu = ['insertColumnAfter', 'deleteRow', 'deleteColumn', 'insertRowAfter', 'splitCell'] 579 | } else { 580 | var td 581 | if (e.target.tagName !== 'TD') { 582 | td = e.target.parentNode 583 | e.target.blur() 584 | } else { 585 | td = e.target 586 | } 587 | banMenu = ['mergeCells', 'splitCell'] 588 | if (getColSpan(td) > 1 || getRowSpan(td) > 1) { 589 | banMenu.splice(banMenu.findIndex(b => b === 'splitCell'), 1) 590 | if (getColSpan(td) > 1) { 591 | banMenu.push('deleteColumn') 592 | } 593 | if (getRowSpan(td) > 1) { 594 | banMenu.push('deleteRow') 595 | } 596 | } 597 | this.clearSelect() 598 | addClass(td, 'selected') 599 | } 600 | // 弹出菜单 601 | let tpl = (() => { 602 | let str = '' 603 | Object.keys(this.menuList).forEach(k => { 604 | if (!banMenu.includes(k)) { 605 | str += `
  • ${this.menuList[k]}
  • ` 606 | } 607 | }) 608 | return str 609 | })() 610 | const menuHtml = `` 611 | const rm = (e, f) => { 612 | if (!f && isParent(e.target, $('#new_context_menu'))) { 613 | return 614 | } 615 | this.clearSelect() 616 | $('#new_context_menu').parentNode.removeChild($('#new_context_menu')) 617 | this.menuState = 'hidden' 618 | d.removeEventListener('mousedown', rm) 619 | } 620 | this.rm = rm 621 | const div = d.createElement('div') 622 | div.id = 'new_context_menu' 623 | div.innerHTML = menuHtml 624 | div.style.left = e.x + 'px' 625 | div.style.visibility = 'hidden' 626 | div.style.top = e.y + 'px' 627 | div.addEventListener('contextmenu', (e) => { 628 | e.preventDefault() 629 | }) 630 | d.body.appendChild(div) 631 | checkIfOutOfScreen(div, e.y) 632 | div.style.visibility = 'visible' 633 | this.bindMenuActions() 634 | d.addEventListener('mousedown', rm) 635 | this.menuState = 'shown' 636 | } 637 | splitCellFunc (td) { 638 | const rowspan = getRowSpan(td) 639 | const colspan = getColSpan(td) 640 | if (colspan > 1) { 641 | for (let i = 1; i < colspan; i++) { 642 | let _td = document.createElement('td') 643 | _td.setAttribute('rowspan', rowspan) 644 | insertAfter(_td, td) 645 | } 646 | td.setAttribute('colspan', 1) 647 | } else { 648 | if (rowspan > 1) { 649 | const tr = td.parentNode 650 | const index = getIndex(tr) 651 | const trs = $('tr', this.el) 652 | for (let i = index + 1; i < index + rowspan; i++) { 653 | trs[i].appendChild(document.createElement('td')) 654 | } 655 | td.setAttribute('rowspan', 1) 656 | } 657 | } 658 | } 659 | bindMenuActions () { 660 | const that = this 661 | if ($('#insertColumnAfter')) { 662 | $('#insertColumnAfter').onclick = function () { 663 | if (hasClass(this, 'disabled')) return 664 | const tds = $('.selected', that.el) 665 | if (tds.length) { 666 | // const last = tds[tds.length - 1] 667 | $('tr', that.el).forEach(v => { 668 | const newtd = d.createElement('td') 669 | newtd.style.width = conf.minWidth + 'px' 670 | v.appendChild(newtd) 671 | // $('td', v).forEach((_td, k) => { 672 | // if (last.offsetLeft === _td.offsetLeft) { 673 | // const newtd = d.createElement('td') 674 | // newtd.style.width = conf.minWidth + 'px' 675 | // insertAfter(newtd, _td) 676 | // } 677 | // }) 678 | }) 679 | } 680 | } 681 | } 682 | if ($('#insertRowAfter')) { 683 | $('#insertRowAfter').onclick = function () { 684 | if (hasClass(this, 'disabled')) return 685 | const tds = $('.selected', that.el) 686 | if (tds.length) { 687 | const last = tds[tds.length - 1] 688 | const trs = $('tr', that.el) 689 | let max = 0 690 | trs.forEach(tr => { 691 | let count = 0 692 | const _tds = $('td', tr) 693 | _tds.forEach(_td => { 694 | count += getColSpan(_td) 695 | }) 696 | max = count > max ? count : max 697 | }) 698 | const tr = d.createElement('tr') 699 | tr.innerHTML = (function () { 700 | let result = '' 701 | for (let i = 0; i < max; i++) { 702 | result += '' 703 | } 704 | return result 705 | })() 706 | const pIndex = getIndex(last.parentNode) + getRowSpan(last) 707 | insertAfter(tr, trs[pIndex - 1]) 708 | } 709 | } 710 | } 711 | if ($('#deleteRow')) { 712 | $('#deleteRow').onclick = function (e) { 713 | if (hasClass(this, 'disabled')) return 714 | const tds = $('.selected', that.el) 715 | if (tds.length) { 716 | const last = tds[tds.length - 1] 717 | let tr = last.parentNode 718 | $('td', tr).forEach(td => { 719 | if (getRowSpan(td) > 1) { 720 | that.splitCellFunc(td) 721 | } 722 | }) 723 | const index = getIndex(tr) 724 | const bigTds = [] 725 | while (tr.previousSibling) { 726 | const _curIndex = getIndex(tr.previousSibling) 727 | $('td', tr.previousSibling).forEach(td => { 728 | if (getRowSpan(td) > index - _curIndex) bigTds.push(td) 729 | }) 730 | tr = tr.previousSibling 731 | } 732 | bigTds.forEach(td => { 733 | td.setAttribute('rowspan', getRowSpan(td) - 1) 734 | }) 735 | last.parentNode.parentNode.removeChild(last.parentNode) 736 | } 737 | that.rm(e, 'force') 738 | } 739 | } 740 | 741 | if ($('#deleteColumn')) { 742 | $('#deleteColumn').onclick = function (e) { 743 | if (hasClass(this, 'disabled')) return 744 | const tds = $('.selected', that.el) 745 | if (tds.length) { 746 | const last = tds[tds.length - 1] 747 | const trs = $('tr', this.el) 748 | trs.forEach(tr => { 749 | $('td', tr).forEach(td => { 750 | if (last.offsetLeft === td.offestLeft) { 751 | that.splitCellFunc(td) 752 | } 753 | }) 754 | }) 755 | const bigTds = [] 756 | const distance = (() => { 757 | let _td = last 758 | let _dis = getColSpan(last) 759 | while (_td.previousSibling) { 760 | _td = _td.previousSibling 761 | _dis += getColSpan(_td) 762 | } 763 | return _dis 764 | })() 765 | trs.forEach(tr => { 766 | let curDis = 0, outloop = false 767 | $('td', tr).forEach((td, k) => { 768 | curDis += getColSpan(td) 769 | if (curDis >= distance && !outloop) { 770 | if (getColSpan(td) > 1) { 771 | bigTds.push(td) 772 | } else { 773 | td.parentNode.removeChild(td) 774 | } 775 | outloop = true 776 | } 777 | }) 778 | }) 779 | bigTds.forEach(td => { 780 | td.setAttribute('colspan', getColSpan(td) - 1) 781 | }) 782 | } 783 | that.rm(e, 'force') 784 | } 785 | } 786 | 787 | if ($('#mergeCells')) { 788 | $('#mergeCells').onclick = function (e) { 789 | if (hasClass(this, 'disabled')) return 790 | const tds = $('.selected', that.el) 791 | if (tds.length > 1) { 792 | let cellNum = 0 793 | if (that.selectDirection === 'x') { 794 | tds.forEach(td => { 795 | cellNum += getColSpan(td) 796 | }) 797 | tds[0].setAttribute('colspan', cellNum) 798 | } else if (that.selectDirection === 'y') { 799 | tds.forEach(td => { 800 | cellNum += getRowSpan(td) 801 | }) 802 | tds[0].setAttribute('rowspan', cellNum) 803 | } else { 804 | console.warn('No cell to merge.') 805 | } 806 | let content = tds[0].innerHTML 807 | tds.forEach((td, k) => { 808 | if (k === 0) return 809 | content += td.innerHTML 810 | td.parentNode.removeChild(td) 811 | }) 812 | tds[0].innerHTML = content 813 | } 814 | that.rm(e, 'force') 815 | } 816 | } 817 | 818 | if ($('#splitCell')) { 819 | $('#splitCell').onclick = function (e) { 820 | if (hasClass(this, 'disabled')) return 821 | const tds = $('.selected', that.el) 822 | if (tds.length) { 823 | const last = tds[tds.length - 1] 824 | that.splitCellFunc(last) 825 | } 826 | that.rm(e, 'force') 827 | } 828 | } 829 | 830 | if ($('#cellStyle')) { 831 | $('#cellStyle').onclick = function () { 832 | if (hasClass(this, 'disabled')) return 833 | const tds = $('.selected', that.el) 834 | if (tds.length) { 835 | that.getTemplate('styleTemplate', function () { 836 | that.bindStyleEvents(tds) 837 | }) 838 | } 839 | } 840 | } 841 | } 842 | 843 | getTemplate (name, callback) { 844 | const container = $('#new_context_menu') 845 | container.style.left = parseInt(container.style.left) + 30 + 'px' 846 | container.innerHTML = this[name]() 847 | checkIfOutOfScreen(container, parseInt(container.style.top)) 848 | callback() 849 | } 850 | 851 | styleTemplate () { 852 | const tds = $('.selected', this.el) 853 | const last = tds[tds.length - 1] 854 | let html = '' 878 | html += '' 879 | return html 880 | } 881 | bindStyleEvents (tds) { 882 | $('select,input', $("#namespace_style_ul")).forEach(el => { 883 | if (el.tagName === 'SELECT') { 884 | el.onchange = function () { 885 | if (el.value !== '') { 886 | tds.forEach(td => { 887 | td.style[el.id.replace('namespace_', '')] = el.value 888 | }) 889 | } 890 | } 891 | } else if (el.tagName === 'INPUT') { 892 | el.oninput = function () { 893 | if (el.value !== '') { 894 | tds.forEach(td => { 895 | td.style[el.id.replace('namespace_', '')] = el.value + 'px' 896 | }) 897 | } 898 | } 899 | } 900 | }) 901 | } 902 | } 903 | 904 | export default TableEditor 905 | --------------------------------------------------------------------------------