├── .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 = ``
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 |
--------------------------------------------------------------------------------