可编辑表格组件开发

This commit is contained in:
pipipi-pikachu 2021-01-20 23:08:51 +08:00
parent 99171297df
commit 6ccbea6cc1
2 changed files with 475 additions and 0 deletions

View File

@ -1,6 +1,10 @@
<template>
<Editor v-if="!screening" />
<Screen v-else />
<div class="test">
<EditableTable />
</div>
</template>
<script lang="ts">
@ -10,12 +14,14 @@ import { MutationTypes, ActionTypes, State } from '@/store'
import Editor from './views/Editor/index.vue'
import Screen from './views/Screen/index.vue'
import EditableTable from '@/components/EditableTable.vue'
export default defineComponent({
name: 'app',
components: {
Editor,
Screen,
EditableTable,
},
setup() {
const store = useStore<State>()
@ -37,4 +43,13 @@ export default defineComponent({
#app {
height: 100%;
}
.test {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 10px;
}
</style>

View File

@ -0,0 +1,460 @@
<template>
<div
class="editable-table"
:style="{ width: width + 'px' }"
>
<table>
<colgroup>
<col span="1" v-for="(width, index) in colWidths" :key="index" :width="width">
</colgroup>
<tbody>
<tr
v-for="(rowCells, rowIndex) in tableCells"
:key="rowIndex"
>
<td
class="cell"
:class="{
'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,
'active': activedCell === `${rowIndex}_${colIndex}`,
}"
v-for="(cell, colIndex) in rowCells"
:key="cell.id"
:rowspan="cell.rowspan"
:colspan="cell.colspan"
:data-cell-index="`${rowIndex}_${colIndex}`"
v-show="!hideCells.includes(`${rowIndex}_${colIndex}`)"
@mousedown="$event => handleCellMousedown($event, rowIndex, colIndex)"
@mouseenter="handleCellMouseenter(rowIndex, colIndex)"
v-contextmenu="el => contextmenus(el)"
>
<div
class="cell-text"
:contenteditable="activedCell === `${rowIndex}_${colIndex}` ? 'plaintext-only' : false"
></div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import { createRandomCode } from '@/utils/common'
import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
import { ContextmenuItem } from './Contextmenu/types'
interface TableCells {
id: string;
colspan: number;
rowspan: number;
text: string;
style?: {
color?: string;
bgColor?: string;
fontSize?: number;
fontName?: string;
bold?: boolean;
italic?: boolean;
align?: string;
};
}
export default defineComponent({
name: 'editable-table',
setup() {
const tableCells = ref<TableCells[][]>([
[
{ id: '1', colspan: 1, rowspan: 1, text: '' },
{ id: '2', colspan: 1, rowspan: 1, text: '' },
{ id: '3', colspan: 1, rowspan: 1, text: '' },
{ id: '4', colspan: 1, rowspan: 1, text: '' },
{ id: '5', colspan: 1, rowspan: 1, text: '' },
],
[
{ id: '6', colspan: 1, rowspan: 1, text: '' },
{ id: '7', colspan: 1, rowspan: 1, text: '' },
{ id: '8', colspan: 1, rowspan: 1, text: '' },
{ id: '9', colspan: 1, rowspan: 1, text: '' },
{ id: '10', colspan: 1, rowspan: 1, text: '' },
],
[
{ id: '11', colspan: 1, rowspan: 1, text: '' },
{ id: '12', colspan: 1, rowspan: 1, text: '' },
{ id: '13', colspan: 1, rowspan: 1, text: '' },
{ id: '14', colspan: 1, rowspan: 1, text: '' },
{ id: '15', colspan: 1, rowspan: 1, text: '' },
],
])
const width = 800
const colWidths = ref([160, 160, 160, 160, 160])
const isStartSelect = ref(false)
const startCell = ref<number[]>([])
const endCell = ref<number[]>([])
const hideCells = computed(() => {
const hideCells = []
for(let i = 0; i < tableCells.value.length; i++) {
const rowCells = tableCells.value[i]
for(let j = 0; j < rowCells.length; j++) {
const cell = rowCells[j]
if(cell.colspan > 1 || cell.rowspan > 1) {
for(let row = i; row < i + cell.rowspan; row++) {
for(let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) {
hideCells.push(`${row}_${col}`)
}
}
}
}
}
return hideCells
})
const selectedCells = computed(() => {
if(!startCell.value.length) return []
const [startX, startY] = startCell.value
if(!endCell.value.length) return [`${startX}_${startY}`]
const [endX, endY] = endCell.value
if(startX === endX && startY === endY) return [`${startX}_${startY}`]
const selectedCells = []
const minX = Math.min(startX, endX)
const minY = Math.min(startY, endY)
const maxX = Math.max(startX, endX)
const maxY = Math.max(startY, endY)
for(let i = 0; i < tableCells.value.length; i++) {
const rowCells = tableCells.value[i]
for(let j = 0; j < rowCells.length; j++) {
if(i >= minX && i <= maxX && j >= minY && j <= maxY) selectedCells.push(`${i}_${j}`)
}
}
return selectedCells
})
const activedCell = computed(() => {
if(selectedCells.value.length > 1) return null
return selectedCells.value[0]
})
const selectedRange = computed(() => {
if(!startCell.value.length) return null
const [startX, startY] = startCell.value
if(!endCell.value.length) return { row: [startX, startX], col: [startY, startY] }
const [endX, endY] = endCell.value
if(startX === endX && startY === endY) return { row: [startX, startX], col: [startY, startY] }
const minX = Math.min(startX, endX)
const minY = Math.min(startY, endY)
const maxX = Math.max(startX, endX)
const maxY = Math.max(startY, endY)
return {
row: [minX, maxX],
col: [minY, maxY],
}
})
const handleMouseup = () => isStartSelect.value = false
const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
if(e.which !== 1) return
endCell.value = []
isStartSelect.value = true
startCell.value = [rowIndex, colIndex]
}
const handleCellMouseenter = (rowIndex: number, colIndex: number) => {
if(!isStartSelect.value) return
endCell.value = [rowIndex, colIndex]
}
onMounted(() => {
document.addEventListener('mouseup', handleMouseup)
})
onUnmounted(() => {
document.removeEventListener('mouseup', handleMouseup)
})
const isHideCell = (rowIndex: number, colIndex: number) => hideCells.value.includes(`${rowIndex}_${colIndex}`)
const removeSelectedCells = () => {
startCell.value = []
endCell.value = []
}
const selectCol = (index: number) => {
const maxRow = tableCells.value.length - 1
startCell.value = [0, index]
endCell.value = [maxRow, index]
}
const selectRow = (index: number) => {
const maxCol = tableCells.value[index].length - 1
startCell.value = [index, 0]
endCell.value = [index, maxCol]
}
const selectAll = () => {
const maxRow = tableCells.value.length - 1
const maxCol = tableCells.value[maxRow].length - 1
startCell.value = [0, 0]
endCell.value = [maxRow, maxCol]
}
const deleteRow = (rowIndex: number) => {
const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
const targetCells = tableCells.value[rowIndex]
const hideCellsPos = []
for(let i = 0; i < targetCells.length; i++) {
if(isHideCell(rowIndex, i)) hideCellsPos.push(i)
}
for(const pos of hideCellsPos) {
for(let i = rowIndex; i >= 0; i--) {
if(!isHideCell(i, pos)) {
_tableCells[i][pos].rowspan = _tableCells[i][pos].rowspan - 1
break
}
}
}
_tableCells.splice(rowIndex, 1)
tableCells.value = _tableCells
}
const deleteCol = (colIndex: number) => {
const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
const hideCellsPos = []
for(let i = 0; i < tableCells.value.length; i++) {
if(isHideCell(i, colIndex)) hideCellsPos.push(i)
}
for(const pos of hideCellsPos) {
for(let i = colIndex; i >= 0; i--) {
if(!isHideCell(pos, i)) {
_tableCells[pos][i].colspan = _tableCells[pos][i].colspan - 1
break
}
}
}
tableCells.value = _tableCells.map(item => {
item.splice(colIndex, 1)
return item
})
}
const insertRow = (selectedIndex: number, rowIndex: number) => {
const rowCells: TableCells[] = []
for(let i = 0; i < tableCells.value[0].length; i++) {
rowCells.push({
colspan: 1,
rowspan: 1,
text: '',
id: createRandomCode(),
})
}
tableCells.value.splice(rowIndex, 0, rowCells)
}
const insertCol = (selectedIndex: number, colIndex: number) => {
tableCells.value = tableCells.value.map(item => {
const cell = {
colspan: 1,
rowspan: 1,
text: '',
id: createRandomCode(),
}
item.splice(colIndex, 0, cell)
return item
})
}
const mergeCells = () => {
const [startX, startY] = startCell.value
const [endX, endY] = endCell.value
const minX = Math.min(startX, endX)
const minY = Math.min(startY, endY)
const maxX = Math.max(startX, endX)
const maxY = Math.max(startY, endY)
const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
_tableCells[minX][minY].rowspan = maxX - minX + 1
_tableCells[minX][minY].colspan = maxY - minY + 1
tableCells.value = _tableCells
removeSelectedCells()
}
const splitCells = (rowIndex: number, colIndex: number) => {
const _tableCells: TableCells[][] = JSON.parse(JSON.stringify(tableCells.value))
_tableCells[rowIndex][colIndex].rowspan = 1
_tableCells[rowIndex][colIndex].colspan = 1
tableCells.value = _tableCells
removeSelectedCells()
}
const getEffectiveTableCells = () => {
const effectiveTableCells = []
for(let i = 0; i < tableCells.value.length; i++) {
const rowCells = tableCells.value[i]
const _rowCells = []
for(let j = 0; j < rowCells.length; j++) {
if(!isHideCell(i, j)) _rowCells.push(rowCells[j])
}
if(_rowCells.length) effectiveTableCells.push(_rowCells)
}
return effectiveTableCells
}
const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
const cellIndex = el.dataset.cellIndex as string
const rowIndex = +cellIndex.split('_')[0]
const colIndex = +cellIndex.split('_')[1]
if(!selectedCells.value.includes(`${rowIndex}_${colIndex}`)) {
startCell.value = [rowIndex, colIndex]
endCell.value = []
}
const isMultiSelected = selectedCells.value.length > 1
const targetCell = tableCells.value[rowIndex][colIndex]
const canSplit = targetCell.rowspan > 1 || targetCell.colspan > 1
const effectiveTableCells = getEffectiveTableCells()
const canDeleteRow = effectiveTableCells.length > 1
const canDeleteCol = effectiveTableCells[0].length > 1
return [
{
text: '插入列',
children: [
{ text: '到左侧', handler: () => insertCol(colIndex, colIndex) },
{ text: '到右侧', handler: () => insertCol(colIndex, colIndex + 1) },
],
},
{
text: '插入行',
children: [
{ text: '到上方', handler: () => insertRow(rowIndex, rowIndex) },
{ text: '到下方', handler: () => insertRow(rowIndex, rowIndex + 1) },
],
},
{
text: '删除列',
disable: !canDeleteCol,
handler: () => deleteCol(colIndex),
},
{
text: '删除行',
disable: !canDeleteRow,
handler: () => deleteRow(rowIndex),
},
{ divider: true },
{
text: '合并单元格',
hide: !isMultiSelected,
handler: mergeCells,
},
{
text: '取消合并单元格',
hide: isMultiSelected || !canSplit,
handler: () => splitCells(rowIndex, colIndex),
},
{
text: '选中全部单元格',
handler: selectAll,
},
]
}
return {
width,
tableCells,
colWidths,
hideCells,
selectedCells,
activedCell,
selectedRange,
handleCellMousedown,
handleCellMouseenter,
selectCol,
selectRow,
contextmenus,
}
},
})
</script>
<style lang="scss" scoped>
.editable-table {
position: relative;
}
table {
width: 100%;
position: relative;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
word-wrap: break-word;
.cell {
padding: 5px;
position: relative;
white-space: normal;
word-wrap: break-word;
vertical-align: middle;
border: 1px solid #d9d9d9;
cursor: default;
&.selected::after {
content: '';
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba($color: #888, $alpha: .1);
}
}
.cell-text {
min-height: 22px;
border: 0;
outline: 0;
line-height: 1.5;
font-size: 14px;
user-select: none;
cursor: text;
&.active {
user-select: text;
}
::selection {
background-color: rgba(27, 110, 232, 0.3);
color: inherit;
}
}
}
</style>