<template>
<div id="testid" style="background-color: goldenrod;">创建表单</div>
<table id='form_table' ref='ref_table' class="form-table" @mousedown.left="handleMouseDown">
<tr>
<td colspan="2">0-0</td>
<td rowspan="2">0-2</td>
<td>0-3</td>
<td>0-4</td>
</tr>
<tr>
<td>1-0</td>
<td>1-1</td>
<td colspan="2">1-3</td>
</tr>
<tr>
<td rowspan="2">2-0</td>
<td>2-1</td>
<td colspan="2">2-2</td>
<td>2-4</td>
</tr>
<tr>
<td>3-1</td>
<td colspan="3">3-2</td>
</tr>
<div id="select_range" :class="rangeBoxStyle" style="pointer-events: none;">
</div>
</table>
<button @click="mergeCell"> 合并单元格 </button>
<button @click="tableInit"> Test </button>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
const ref_table = ref<HTMLTableElement>()
const rangeBoxStyle = ref<string>('range-box-none')
const startTd = ref<HTMLTableCellElement>()
const endTd = ref<HTMLTableCellElement>()
const cellRange = reactive({
start_td: { a: { x: 0, y: 0 }, b: { x: 0, y: 0 }, c: { x: 0, y: 0 }, d: { x: 0, y: 0 } }, // 第一个选取 td 三个角的座标
end_td: { a: { x: 0, y: 0 }, b: { x: 0, y: 0 }, c: { x: 0, y: 0 }, d: { x: 0, y: 0 } }, // 最后一个选取 td 三个角的座标
range_box: { left: '', top: '', width: '', height: '' }, // 框选 DIV
})
const MMRC = reactive({ startRowIndex: -1, startCellIndex: -1, endRowIndex: -1, endCellIndex: -1 })
function removeElement(_element: any) {
const _parentElement = _element.parentNode;
if (_parentElement) {
_parentElement.removeChild(_element);
}
}
//初始化单元格
function tableInit() {
// 获取表格对象
const tb = ref_table.value as HTMLTableElement
//补全表格的td
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
if (tb_td.colSpan > 1) { // 假如 当前td 的 colSpan > 1
for (let a = 0; a < tb_td.colSpan - 1; a++) { tb.rows[i].insertCell(x + a + 1).className = 'td-hidden' }
}
if (tb_td.rowSpan > 1) { // 假如 当前td 的 rowSpan > 1
// tb_td.rowSpan - 1 当前td, 要向下合并的行数
for (let y = i + 1; y < i + 1 + tb_td.rowSpan - 1; y++) { //
tb.rows[y].insertCell(x).className = 'td-hidden'
}
}
}
}
// 给每个单元格做上标记 td_rc ={ r:初始行数 , c:初始列数, maxc:本列列数+要合并的列数 , maxr:本行行数 + 要合并的行数 }
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
tb_td.setAttribute('td_rc', `{"r":${i},"c":${x},"maxc":${x + tb_td.colSpan - 1},"maxr":${i + tb_td.rowSpan - 1}}`)
}
}
const td_hidden = document.getElementsByClassName('td-hidden')
for (let len = td_hidden.length, i = len - 1; i >= 0; i--) {
removeElement(td_hidden[i])
}
}
// 检查table的完整性
function checkMergeTable(table_id: string) {
// 获取表格对象
const tb = document.getElementById(table_id) as HTMLTableElement
if (tb.rows.length == 0) return false;
if (tb.rows[0].cells.length == 0) return false;
// 表格总列数计算
let col_total = 0
for (let i = 0; i < tb.rows[0].children.length; i++) { // count columns of first row
col_total = col_total + (tb.rows[0].children[i] as HTMLTableCellElement).colSpan // 表格总列数
}
// 单元格没有合并前,表格每行原始列数: 对象: row_span ={'行号':列数','行号':列数','行号':列数',...}
const row_span: any = {}
for (let i = 0; i < tb.rows.length; i++) {
row_span[i] = col_total
}
// 根据表格Td的 rowSpan colSpan 属性,改写 row_span 对象的值
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
if (tb_td.colSpan > 1) { // 假如 当前td 的 colSpan > 1
row_span[i] = row_span[i] - tb_td.colSpan + 1 // 则减少相应的列
}
if (tb_td.rowSpan > 1) { // 假如 当前td 的 rowSpan > 1
const margin_row = tb_td.rowSpan - 1 // 当前td, 要向下合并的行数
for (let y = i + 1; y <= i + margin_row; y++) { // i+1 行 至 i+ margin_row 行, 每行则 减少 1列
row_span[y] = row_span[y] - 1
}
}
}
}
// 比对列数
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
if ((tb.rows[i] as HTMLTableRowElement).cells.length != row_span[i]) { // 假如 列数不对
console.log('表格错误!',row_span[i])
console.log('tb.rows[i].children',tb.rows[i].children)
console.log('tb.rows[i].cells', tb.rows[i].cells,)
return false
}
}
console.log('表格检查', row_span)
return true;
}
function mergeCell() {
checkMergeTable('form_table')
const tb = document.getElementById('form_table') as HTMLTableElement
// 计算出 绿色边框的div 位置 和 宽高
const sTD = startTd.value as HTMLTableCellElement
const eTD = endTd.value as HTMLTableCellElement
const sRC = JSON.parse(sTD.getAttribute('td_rc') as string)
const eRC = JSON.parse(eTD.getAttribute('td_rc') as string)
const s_row = sRC.r // 起始行
const e_row = eRC.maxr // 结束行
const rowList = ref_table.value?.children as HTMLCollection
let merge_row = Math.abs(sRC.r - eRC.maxr) + 1
let merge_col = Math.abs(sRC.c - eRC.maxc) + 1
sTD.rowSpan = merge_row
sTD.colSpan = merge_col
if(sTD == eTD) return
for (let i = s_row; i <= e_row; i++) {
console.log('.....i..', i)
for (let x = rowList[i].children.length - 1; x>=0; x--) {
const td = rowList[i].children[x]
const td_rc = JSON.parse((td as HTMLTableCellElement).getAttribute('td_rc') as string)
if (td != sTD && td_rc.r >= s_row && td_rc.r <= eRC.maxr && td_rc.c >= sRC.c && td_rc.c <= eRC.maxc) {
(rowList[i] as HTMLTableRowElement).deleteCell(x)
}
}
}
console.log('sTD',sTD)
checkMergeTable('form_table')
// 给当前元素套一层, 绿色边框的div, 并显示出来
cellRange.range_box.left = (sTD.offsetLeft - 1) + 'px' // 绿色边框的div position 定位的 left, 值绑定在 style 类属性中
cellRange.range_box.top = (sTD.offsetTop - 1) + 'px' // 绿色边框的div position 定位的 top, 值绑定在 style 类属性中
cellRange.range_box.width = (sTD.offsetWidth + 1) + 'px' // 绿色边框的div 宽度
cellRange.range_box.height = (sTD.offsetHeight + 2) + 'px' // 绿色边框的div 高度
}
//框选单元格操作
const handleMouseDown = (event: any) => {
if (event.target.nodeName != 'TD') return
console.log('第一个单元格', event.target)
tableInit()
startTd.value = event.target // first Dom
const sTD = startTd.value as HTMLTableCellElement
// 记录第一个点击目标 4个角的座标 x,y 由于框选的方向不一样,所以要记录4个角的座标 x,y
cellRange.start_td = {
a: { x: event.target.offsetLeft, y: event.target.offsetTop },
b: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop },
c: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop + event.target.offsetHeight },
d: { x: event.target.offsetLeft, y: event.target.offsetTop + event.target.offsetHeight },
}
// 给当前元素套一层, 绿色边框的div, 并显示出来
cellRange.range_box.left = (sTD.offsetLeft - 1) + 'px' // 绿色边框的div position 定位的 left, 值绑定在 style 类属性中
cellRange.range_box.top = (sTD.offsetTop - 1) + 'px'
cellRange.range_box.width = (sTD.offsetWidth + 1) + 'px' // 绿色边框的div 宽度
cellRange.range_box.height = (sTD.offsetHeight + 2) + 'px' // 绿色边框的div 高度
rangeBoxStyle.value = 'range-box-show' // 显示出来 (更改类名来显示)
const form_table = document.getElementById('form_table') as HTMLTableElement
form_table.addEventListener("mousemove", handleMouseMove) //监听鼠标移动事件
form_table.addEventListener("mouseup", handleMouseUp) //监听鼠标抬起事件
}
function handleMouseUp(event: any) {
const tb = ref_table.value
const form_table = document.getElementById('form_table') as HTMLTableElement
form_table.removeEventListener("mousemove", handleMouseMove);
form_table.removeEventListener("mouseup", handleMouseUp);
// cellRange.is_show_mask = false;
}
function addSelectedClass() {
let selected = false, t;
const tb = ref_table.value as HTMLTableElement
for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
const rc = JSON.parse(tb_td.getAttribute('td_rc') as string)
selected = rc.r >= MMRC.startRowIndex && rc.r <= MMRC.endRowIndex && rc.c >= MMRC.startCellIndex && rc.c <= MMRC.endCellIndex;
if (!selected && rc.maxc) { //合并过的单元格,判断另外3(左下,右上,右下)个角的行列是否在区域内
selected =
(rc.maxr >= MMRC.startRowIndex && rc.maxr <= MMRC.endRowIndex && rc.c >= MMRC.startCellIndex && rc.c <= MMRC.endCellIndex) ||//左下
(rc.r >= MMRC.startRowIndex && rc.r <= MMRC.endRowIndex && rc.maxc >= MMRC.startCellIndex && rc.maxc <= MMRC.endCellIndex) ||//右上
(rc.maxr >= MMRC.startRowIndex && rc.maxr <= MMRC.endRowIndex && rc.maxc >= MMRC.startCellIndex && rc.maxc <= MMRC.endCellIndex);//右下
}
if (selected) {
tb_td.setAttribute('className', 'selected')
}
}
}
// let rangeChange = false;
// for (let i = 0; i < tb.rows.length; i++) { // 循环 tabel每行 row
// for (let x = 0; x < tb.rows[i].children.length; x++) { // 循环每行的 td
// const tb_td = tb.rows[i].children[x] as HTMLTableCellElement
// const rc = JSON.parse(tb_td.getAttribute('td_rc') as string)
// t = MMRC.startRowIndex;
// MMRC.startRowIndex = Math.min(MMRC.startRowIndex, rc.r);
// rangeChange = rangeChange || MMRC.startRowIndex != t;
// t = MMRC.endRowIndex;
// MMRC.endRowIndex = Math.max(MMRC.endRowIndex, rc.maxr || rc.r);
// rangeChange = rangeChange || MMRC.endRowIndex != t;
// t = MMRC.startCellIndex;
// MMRC.startCellIndex = Math.min(MMRC.startCellIndex, rc.c);
// rangeChange = rangeChange || MMRC.startCellIndex != t;
// t = MMRC.endCellIndex;
// MMRC.endCellIndex = Math.max(MMRC.endCellIndex, rc.maxc || rc.c);
// rangeChange = rangeChange || MMRC.endCellIndex != t;
// }
// }
// //注意这里如果用代码选中过合并的单元格需要重新执行选中操作
// if (rangeChange) addSelectedClass()
// console.log('selected', selected)
}
function handleMouseMove(event: any) {
// 根据鼠标移动的位置来调整,绿色边框的div的大小和位置:
if (event.target.nodeName != 'TD') return
endTd.value = event.target as HTMLTableCellElement // last Dom
cellRange.end_td = { // 当前鼠标移动到的目标位置
a: { x: event.target.offsetLeft, y: event.target.offsetTop },
b: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop },
c: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop + event.target.offsetHeight },
d: { x: event.target.offsetLeft, y: event.target.offsetTop + event.target.offsetHeight },
}
if (cellRange.end_td.a.x >= cellRange.start_td.a.x) { // 右移
cellRange.range_box.left = (cellRange.start_td.a.x - 1) + 'px'
if (cellRange.end_td.b.x < cellRange.start_td.b.x) {
console.log('右.......')
cellRange.range_box.width = (cellRange.start_td.b.x - cellRange.start_td.a.x + 1) + 'px'
} else {
cellRange.range_box.width = (cellRange.end_td.c.x - cellRange.start_td.a.x + 1) + 'px' // 绿色边框的div 宽度
}
if (cellRange.end_td.a.y >= cellRange.start_td.a.y) { // 右下
cellRange.range_box.top = (cellRange.start_td.a.y - 1) + 'px' // 绿色边框的div position 定位的 top
cellRange.range_box.height = (cellRange.end_td.c.y - cellRange.start_td.a.y + 2) + 'px' // 绿色边框的div 高度
} else { // 右上
cellRange.range_box.top = (cellRange.end_td.a.y - 1) + 'px'
cellRange.range_box.height = (cellRange.start_td.c.y - cellRange.end_td.a.y + 2) + 'px'
}
} else { // 左移
console.log('左.......')
cellRange.range_box.left = (cellRange.end_td.a.x - 1) + 'px'
cellRange.range_box.width = (cellRange.start_td.b.x - cellRange.end_td.a.x + 1) + 'px'
if (cellRange.end_td.c.y <= cellRange.start_td.c.y) { // 左上
console.log('左上')
cellRange.range_box.top = (cellRange.end_td.a.y - 1) + 'px'
cellRange.range_box.height = (cellRange.start_td.c.y - cellRange.end_td.a.y + 2) + 'px'
} else { // 左下
console.log('左下')
cellRange.range_box.top = (cellRange.start_td.b.y - 1) + 'px'
cellRange.range_box.height = (cellRange.end_td.d.y - cellRange.start_td.b.y + 2) + 'px'
}
}
}
const handleMouseDown_1 = (event: any) => {
console.log('点了这个单元格:', document.styleSheets[0])
if (event.target.nodeName != 'TD') return
tableInit()
// 记录第一个点击目标 4个角的座标 x,y 由于框选的方向不一样,所以要记录4个角的座标 x,y
startTd.value = event.target // first Dom
cellRange.start_td = {
a: { x: event.target.offsetLeft, y: event.target.offsetTop },
b: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop },
c: { x: event.target.offsetLeft + event.target.offsetWidth, y: event.target.offsetTop + event.target.offsetHeight },
d: { x: event.target.offsetLeft, y: event.target.offsetTop + event.target.offsetHeight },
}
// 给当前元素套一层, 绿色边框的div, 并显示出来
cellRange.range_box.left = (cellRange.start_td.a.x - 1) + 'px' // 绿色边框的div position 定位的 left, 值绑定在 style 类属性中
cellRange.range_box.top = (cellRange.start_td.a.y - 1) + 'px' // 绿色边框的div position 定位的 top, 值绑定在 style 类属性中
cellRange.range_box.width = (event.target.offsetWidth + 1) + 'px' // 绿色边框的div 宽度
cellRange.range_box.height = (event.target.offsetHeight + 2) + 'px' // 绿色边框的div 高度
rangeBoxStyle.value = 'range-box-show' // 显示出来 (更改类名来显示)
const form_table = document.getElementById('form_table') as HTMLTableElement
// const testobj = document.getElementById('testid') as HTMLDivElement
form_table.addEventListener("mousemove", handleMouseMove) //监听鼠标移动事件
form_table.addEventListener("mouseup", handleMouseUp) //监听鼠标抬起事件
// console.log(`第一个对象clientTop:${event.target.id}`, form_table.getBoundingClientRect())
// console.log('点了这个单元格:', JSON.parse(event.target.getAttribute('td_rc')))
}
function handleMouseUp_1(event: any) {
// endTd.value = event.target // last Dom
// console.log(event.target.innerHTML, event.target.parentElement.rowIndex, event.target.cellIndex)
// rangeBoxStyle.value = 'range-box-none'
const form_table = document.getElementById('form_table') as HTMLTableElement
form_table.removeEventListener("mousemove", handleMouseMove);
form_table.removeEventListener("mouseup", handleMouseUp);
// cellRange.is_show_mask = false;
}
onMounted(() => {
})
</script>
<style lang="scss">
@import '../../assets/css/handle';
.range-box-none {
display: none;
position: absolute;
}
.range-box-show {
border: 2px solid rgb(66, 166, 66);
display: block;
position: absolute;
left: v-bind('cellRange.range_box.left');
top: v-bind('cellRange.range_box.top'); // offsetTop
width: v-bind('cellRange.range_box.width'); // offsetLeft
height: v-bind('cellRange.range_box.height'); // offsetLeft
}
.form-table {
margin: auto;
margin-top: 100px;
font-size: 12px;
position: relative;
// 让文字不可选
user-select: none;
-webkit-user-seletct: none;
-moz-user-seletct: none;
td {
border: 0.5px solid;
}
td {
width: 200px;
}
.td-hidden {
// display: none;
background-color: rgb(152, 152, 152);
}
// .td-hidden::before {
// position: absolute;
// content: '你好';
// color: white;
// }
// td::before {
// position: relative;
// left: -10px;
// content: attr(td_rc);
// color: rgb(234, 173, 94);
// }
}
</style>