| | <template> |
| | <div |
| | class="editable-table" |
| | :style="{ width: totalWidth + 'px' }" |
| | > |
| | <div class="handler" v-if="editable"> |
| | <div |
| | class="drag-line" |
| | v-for="(pos, index) in dragLinePosition" |
| | :key="index" |
| | :style="{ left: pos + 'px' }" |
| | @mousedown="$event => handleMousedownColHandler($event, index)" |
| | ></div> |
| | </div> |
| | <table |
| | :class="{ |
| | 'theme': theme, |
| | 'row-header': theme?.rowHeader, |
| | 'row-footer': theme?.rowFooter, |
| | 'col-header': theme?.colHeader, |
| | 'col-footer': theme?.colFooter, |
| | }" |
| | :style="`--themeColor: ${theme?.color}; --subThemeColor1: ${subThemeColor[0]}; --subThemeColor2: ${subThemeColor[1]}`" |
| | > |
| | <colgroup> |
| | <col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width"> |
| | </colgroup> |
| | <tbody> |
| | <tr v-for="(rowCells, rowIndex) in tableCells" :key="rowIndex" :style="{ height: cellMinHeight + 'px' }"> |
| | <td |
| | class="cell" |
| | :class="{ |
| | 'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1, |
| | 'active': activedCell === `${rowIndex}_${colIndex}`, |
| | }" |
| | :style="{ |
| | borderStyle: outline.style, |
| | borderColor: outline.color, |
| | borderWidth: outline.width + 'px', |
| | ...getTextStyle(cell.style), |
| | }" |
| | 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: HTMLElement) => contextmenus(el)" |
| | > |
| | <CustomTextarea |
| | v-if="activedCell === `${rowIndex}_${colIndex}`" |
| | class="cell-text" |
| | :class="{ 'active': activedCell === `${rowIndex}_${colIndex}` }" |
| | :style="{ minHeight: (cellMinHeight - 4) + 'px' }" |
| | :value="cell.text" |
| | @updateValue="value => handleInput(value, rowIndex, colIndex)" |
| | @insertExcelData="value => insertExcelData(value, rowIndex, colIndex)" |
| | /> |
| | <div v-else class="cell-text" :style="{ minHeight: (cellMinHeight - 4) + 'px' }" v-html="formatText(cell.text)" /> |
| | </td> |
| | </tr> |
| | </tbody> |
| | </table> |
| | </div> |
| | </template> |
| | |
| | <script lang="ts" setup> |
| | import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' |
| | import { debounce, isEqual } from 'lodash' |
| | import { storeToRefs } from 'pinia' |
| | import { nanoid } from 'nanoid' |
| | import { useMainStore } from '@/store' |
| | import type { PPTElementOutline, TableCell, TableTheme } from '@/types/slides' |
| | import type { ContextmenuItem } from '@/components/Contextmenu/types' |
| | import { KEYS } from '@/configs/hotkey' |
| | import { getTextStyle, formatText } from './utils' |
| | import useHideCells from './useHideCells' |
| | import useSubThemeColor from './useSubThemeColor' |
| | |
| | import CustomTextarea from './CustomTextarea.vue' |
| | |
| | const props = withDefaults(defineProps<{ |
| | data: TableCell[][] |
| | width: number |
| | cellMinHeight: number |
| | colWidths: number[] |
| | outline: PPTElementOutline |
| | theme?: TableTheme |
| | editable?: boolean |
| | }>(), { |
| | editable: true, |
| | }) |
| | |
| | const emit = defineEmits<{ |
| | (event: 'change', payload: TableCell[][]): void |
| | (event: 'changeColWidths', payload: number[]): void |
| | (event: 'changeSelectedCells', payload: string[]): void |
| | }>() |
| | |
| | const { canvasScale } = storeToRefs(useMainStore()) |
| | |
| | const isStartSelect = ref(false) |
| | const startCell = ref<number[]>([]) |
| | const endCell = ref<number[]>([]) |
| | |
| | const tableCells = computed<TableCell[][]>({ |
| | get() { |
| | return props.data |
| | }, |
| | set(newData) { |
| | emit('change', newData) |
| | }, |
| | }) |
| | |
| | |
| | const theme = computed(() => props.theme) |
| | const { subThemeColor } = useSubThemeColor(theme) |
| | |
| | |
| | const colSizeList = ref<number[]>([]) |
| | const totalWidth = computed(() => colSizeList.value.reduce((a, b) => a + b)) |
| | watch([ |
| | () => props.colWidths, |
| | () => props.width, |
| | ], () => { |
| | colSizeList.value = props.colWidths.map(item => item * props.width) |
| | }, { immediate: true }) |
| | |
| | |
| | |
| | const removeSelectedCells = () => { |
| | startCell.value = [] |
| | endCell.value = [] |
| | } |
| | |
| | watch(() => props.editable, () => { |
| | if (!props.editable) removeSelectedCells() |
| | }) |
| | |
| | |
| | const dragLinePosition = computed(() => { |
| | const dragLinePosition: number[] = [] |
| | for (let i = 1; i < colSizeList.value.length + 1; i++) { |
| | const pos = colSizeList.value.slice(0, i).reduce((a, b) => (a + b)) |
| | dragLinePosition.push(pos) |
| | } |
| | return dragLinePosition |
| | }) |
| | |
| | |
| | const cells = computed(() => props.data) |
| | const { hideCells } = useHideCells(cells) |
| | |
| | |
| | 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 |
| | }) |
| | |
| | watch(selectedCells, (value, oldValue) => { |
| | if (isEqual(value, oldValue)) return |
| | emit('changeSelectedCells', selectedCells.value) |
| | }) |
| | |
| | |
| | const activedCell = computed(() => { |
| | if (selectedCells.value.length > 1) return null |
| | return selectedCells.value[0] |
| | }) |
| | |
| | |
| | const handleMouseup = () => isStartSelect.value = false |
| | |
| | const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => { |
| | if (e.button === 0) { |
| | 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 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: TableCell[][] = 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: TableCell[][] = 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 |
| | }) |
| | colSizeList.value.splice(colIndex, 1) |
| | emit('changeColWidths', colSizeList.value) |
| | } |
| | |
| | |
| | const insertRow = (rowIndex: number) => { |
| | const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value)) |
| | |
| | const rowCells: TableCell[] = [] |
| | for (let i = 0; i < _tableCells[0].length; i++) { |
| | rowCells.push({ |
| | colspan: 1, |
| | rowspan: 1, |
| | text: '', |
| | id: nanoid(10), |
| | }) |
| | } |
| | |
| | _tableCells.splice(rowIndex, 0, rowCells) |
| | tableCells.value = _tableCells |
| | } |
| | |
| | |
| | const insertCol = (colIndex: number) => { |
| | tableCells.value = tableCells.value.map(item => { |
| | const cell = { |
| | colspan: 1, |
| | rowspan: 1, |
| | text: '', |
| | id: nanoid(10), |
| | } |
| | item.splice(colIndex, 0, cell) |
| | return item |
| | }) |
| | colSizeList.value.splice(colIndex, 0, 100) |
| | emit('changeColWidths', colSizeList.value) |
| | } |
| | |
| | |
| | const fillTable = (rowCount: number, colCount: number) => { |
| | let _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value)) |
| | const defaultCell = { colspan: 1, rowspan: 1, text: '' } |
| | |
| | if (rowCount) { |
| | const newRows = [] |
| | for (let i = 0; i < rowCount; i++) { |
| | const rowCells: TableCell[] = [] |
| | for (let j = 0; j < _tableCells[0].length; j++) { |
| | rowCells.push({ |
| | ...defaultCell, |
| | id: nanoid(10), |
| | }) |
| | } |
| | newRows.push(rowCells) |
| | } |
| | _tableCells = [..._tableCells, ...newRows] |
| | } |
| | if (colCount) { |
| | _tableCells = _tableCells.map(item => { |
| | const cells: TableCell[] = [] |
| | for (let i = 0; i < colCount; i++) { |
| | const cell = { |
| | ...defaultCell, |
| | id: nanoid(10), |
| | } |
| | cells.push(cell) |
| | } |
| | return [...item, ...cells] |
| | }) |
| | colSizeList.value = [...colSizeList.value, ...new Array(colCount).fill(100)] |
| | emit('changeColWidths', colSizeList.value) |
| | } |
| | |
| | tableCells.value = _tableCells |
| | } |
| | |
| | |
| | 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: TableCell[][] = 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: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value)) |
| | _tableCells[rowIndex][colIndex].rowspan = 1 |
| | _tableCells[rowIndex][colIndex].colspan = 1 |
| | |
| | tableCells.value = _tableCells |
| | removeSelectedCells() |
| | } |
| | |
| | |
| | const handleMousedownColHandler = (e: MouseEvent, colIndex: number) => { |
| | removeSelectedCells() |
| | let isMouseDown = true |
| | |
| | const originWidth = colSizeList.value[colIndex] |
| | const startPageX = e.pageX |
| | |
| | const minWidth = 50 |
| | |
| | document.onmousemove = e => { |
| | if (!isMouseDown) return |
| | |
| | const moveX = (e.pageX - startPageX) / canvasScale.value |
| | const width = originWidth + moveX < minWidth ? minWidth : Math.round(originWidth + moveX) |
| | |
| | colSizeList.value[colIndex] = width |
| | } |
| | document.onmouseup = () => { |
| | isMouseDown = false |
| | document.onmousemove = null |
| | document.onmouseup = null |
| | |
| | emit('changeColWidths', colSizeList.value) |
| | } |
| | } |
| | |
| | |
| | const clearSelectedCellText = () => { |
| | const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value)) |
| | |
| | for (let i = 0; i < _tableCells.length; i++) { |
| | for (let j = 0; j < _tableCells[i].length; j++) { |
| | if (selectedCells.value.includes(`${i}_${j}`)) { |
| | _tableCells[i][j].text = '' |
| | } |
| | } |
| | } |
| | tableCells.value = _tableCells |
| | } |
| | |
| | const focusActiveCell = () => { |
| | nextTick(() => { |
| | const textRef = document.querySelector('.cell-text.active') as HTMLInputElement |
| | if (textRef) textRef.focus() |
| | }) |
| | } |
| | |
| | |
| | |
| | |
| | |
| | const tabActiveCell = () => { |
| | const getNextCell = (i: number, j: number): [number, number] | null => { |
| | if (!tableCells.value[i]) return null |
| | if (!tableCells.value[i][j]) return getNextCell(i + 1, 0) |
| | if (isHideCell(i, j)) return getNextCell(i, j + 1) |
| | return [i, j] |
| | } |
| | |
| | endCell.value = [] |
| | |
| | const nextRow = startCell.value[0] |
| | const nextCol = startCell.value[1] + 1 |
| | |
| | const nextCell = getNextCell(nextRow, nextCol) |
| | if (!nextCell) { |
| | insertRow(nextRow + 1) |
| | startCell.value = [nextRow + 1, 0] |
| | } |
| | else startCell.value = nextCell |
| | |
| | |
| | focusActiveCell() |
| | } |
| | |
| | |
| | const moveActiveCell = (dir: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT') => { |
| | const rowIndex = +selectedCells.value[0].split('_')[0] |
| | const colIndex = +selectedCells.value[0].split('_')[1] |
| | |
| | const rowLen = tableCells.value.length |
| | const colLen = tableCells.value[0].length |
| | |
| | const getEffectivePos = (pos: [number, number]): [number, number] => { |
| | if (pos[0] < 0 || pos[1] < 0 || pos[0] > rowLen - 1 || pos[1] > colLen - 1) return [0, 0] |
| | |
| | const p = `${pos[0]}_${pos[1]}` |
| | if (!hideCells.value.includes(p)) return pos |
| | |
| | if (dir === 'UP') { |
| | return getEffectivePos([pos[0], pos[1] - 1]) |
| | } |
| | if (dir === 'DOWN') { |
| | return getEffectivePos([pos[0], pos[1] - 1]) |
| | } |
| | if (dir === 'LEFT') { |
| | return getEffectivePos([pos[0] - 1, pos[1]]) |
| | } |
| | if (dir === 'RIGHT') { |
| | return getEffectivePos([pos[0] - 1, pos[1]]) |
| | } |
| | |
| | return [0, 0] |
| | } |
| | |
| | if (dir === 'UP') { |
| | const _rowIndex = rowIndex - 1 |
| | if (_rowIndex < 0) return |
| | endCell.value = [] |
| | startCell.value = getEffectivePos([_rowIndex, colIndex]) |
| | } |
| | else if (dir === 'DOWN') { |
| | const _rowIndex = rowIndex + 1 |
| | if (_rowIndex > rowLen - 1) return |
| | endCell.value = [] |
| | startCell.value = getEffectivePos([_rowIndex, colIndex]) |
| | } |
| | else if (dir === 'LEFT') { |
| | const _colIndex = colIndex - 1 |
| | if (_colIndex < 0) return |
| | endCell.value = [] |
| | startCell.value = getEffectivePos([rowIndex, _colIndex]) |
| | } |
| | else if (dir === 'RIGHT') { |
| | const _colIndex = colIndex + 1 |
| | if (_colIndex > colLen - 1) return |
| | endCell.value = [] |
| | startCell.value = getEffectivePos([rowIndex, _colIndex]) |
| | } |
| | |
| | focusActiveCell() |
| | } |
| | |
| | |
| | const getCaretPosition = (element: HTMLDivElement) => { |
| | const selection = window.getSelection() |
| | if (selection && selection.rangeCount > 0) { |
| | const range = selection.getRangeAt(0) |
| | |
| | const preCaretRange = range.cloneRange() |
| | preCaretRange.selectNodeContents(element) |
| | |
| | preCaretRange.setEnd(range.startContainer, range.startOffset) |
| | const start = preCaretRange.toString().length |
| | preCaretRange.setEnd(range.endContainer, range.endOffset) |
| | const end = preCaretRange.toString().length |
| | |
| | const len = element.textContent?.length || 0 |
| | |
| | return { start, end, len } |
| | } |
| | return null |
| | } |
| | |
| | |
| | const keydownListener = (e: KeyboardEvent) => { |
| | if (!props.editable || !selectedCells.value.length) return |
| | |
| | const key = e.key.toUpperCase() |
| | if (selectedCells.value.length < 2) { |
| | if (key === KEYS.TAB) { |
| | e.preventDefault() |
| | tabActiveCell() |
| | } |
| | else if (e.ctrlKey && key === KEYS.UP) { |
| | e.preventDefault() |
| | const rowIndex = +selectedCells.value[0].split('_')[0] |
| | insertRow(rowIndex) |
| | } |
| | else if (e.ctrlKey && key === KEYS.DOWN) { |
| | e.preventDefault() |
| | const rowIndex = +selectedCells.value[0].split('_')[0] |
| | insertRow(rowIndex + 1) |
| | } |
| | else if (e.ctrlKey && key === KEYS.LEFT) { |
| | e.preventDefault() |
| | const colIndex = +selectedCells.value[0].split('_')[1] |
| | insertCol(colIndex) |
| | } |
| | else if (e.ctrlKey && key === KEYS.RIGHT) { |
| | e.preventDefault() |
| | const colIndex = +selectedCells.value[0].split('_')[1] |
| | insertCol(colIndex + 1) |
| | } |
| | else if (key === KEYS.UP) { |
| | const range = getCaretPosition(e.target as HTMLDivElement) |
| | if (range && range.start === range.end && range.start === 0) { |
| | moveActiveCell('UP') |
| | } |
| | } |
| | else if (key === KEYS.DOWN) { |
| | const range = getCaretPosition(e.target as HTMLDivElement) |
| | if (range && range.start === range.end && range.start === range.len) { |
| | moveActiveCell('DOWN') |
| | } |
| | } |
| | else if (key === KEYS.LEFT) { |
| | const range = getCaretPosition(e.target as HTMLDivElement) |
| | if (range && range.start === range.end && range.start === 0) { |
| | moveActiveCell('LEFT') |
| | } |
| | } |
| | else if (key === KEYS.RIGHT) { |
| | const range = getCaretPosition(e.target as HTMLDivElement) |
| | if (range && range.start === range.end && range.start === range.len) { |
| | moveActiveCell('RIGHT') |
| | } |
| | } |
| | } |
| | else if (key === KEYS.DELETE) { |
| | clearSelectedCellText() |
| | } |
| | } |
| | |
| | onMounted(() => { |
| | document.addEventListener('keydown', keydownListener) |
| | }) |
| | onUnmounted(() => { |
| | document.removeEventListener('keydown', keydownListener) |
| | }) |
| | |
| | |
| | const handleInput = debounce(function(value, rowIndex, colIndex) { |
| | tableCells.value[rowIndex][colIndex].text = value |
| | emit('change', tableCells.value) |
| | }, 300, { trailing: true }) |
| | |
| | |
| | const insertExcelData = (data: string[][], rowIndex: number, colIndex: number) => { |
| | const maxRow = data.length |
| | const maxCol = data[0].length |
| | |
| | let fillRowCount = 0 |
| | let fillColCount = 0 |
| | if (rowIndex + maxRow > tableCells.value.length) fillRowCount = rowIndex + maxRow - tableCells.value.length |
| | if (colIndex + maxCol > tableCells.value[0].length) fillColCount = colIndex + maxCol - tableCells.value[0].length |
| | if (fillRowCount || fillColCount) fillTable(fillRowCount, fillColCount) |
| | |
| | nextTick(() => { |
| | for (let i = 0; i < maxRow; i++) { |
| | for (let j = 0; j < maxCol; j++) { |
| | if (tableCells.value[rowIndex + i][colIndex + j]) { |
| | tableCells.value[rowIndex + i][colIndex + j].text = data[i][j] |
| | } |
| | } |
| | } |
| | emit('change', tableCells.value) |
| | }) |
| | } |
| | |
| | |
| | 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 checkCanDeleteRowOrCol = () => { |
| | const effectiveTableCells = getEffectiveTableCells() |
| | const canDeleteRow = effectiveTableCells.length > 1 |
| | const canDeleteCol = effectiveTableCells[0].length > 1 |
| | |
| | return { canDeleteRow, canDeleteCol } |
| | } |
| | |
| | |
| | |
| | |
| | const checkCanMergeOrSplit = (rowIndex: number, colIndex: number) => { |
| | const isMultiSelected = selectedCells.value.length > 1 |
| | const targetCell = tableCells.value[rowIndex][colIndex] |
| | |
| | const canMerge = isMultiSelected |
| | const canSplit = !isMultiSelected && (targetCell.rowspan > 1 || targetCell.colspan > 1) |
| | |
| | return { canMerge, canSplit } |
| | } |
| | |
| | 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 { canMerge, canSplit } = checkCanMergeOrSplit(rowIndex, colIndex) |
| | const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol() |
| | |
| | return [ |
| | { |
| | text: '插入列', |
| | children: [ |
| | { text: '到左侧', handler: () => insertCol(colIndex) }, |
| | { text: '到右侧', handler: () => insertCol(colIndex + 1) }, |
| | ], |
| | }, |
| | { |
| | text: '插入行', |
| | children: [ |
| | { text: '到上方', handler: () => insertRow(rowIndex) }, |
| | { text: '到下方', handler: () => insertRow(rowIndex + 1) }, |
| | ], |
| | }, |
| | { |
| | text: '删除列', |
| | disable: !canDeleteCol, |
| | handler: () => deleteCol(colIndex), |
| | }, |
| | { |
| | text: '删除行', |
| | disable: !canDeleteRow, |
| | handler: () => deleteRow(rowIndex), |
| | }, |
| | { divider: true }, |
| | { |
| | text: '合并单元格', |
| | disable: !canMerge, |
| | handler: mergeCells, |
| | }, |
| | { |
| | text: '取消合并单元格', |
| | disable: !canSplit, |
| | handler: () => splitCells(rowIndex, colIndex), |
| | }, |
| | { divider: true }, |
| | { |
| | text: '选中当前列', |
| | handler: () => selectCol(colIndex), |
| | }, |
| | { |
| | text: '选中当前行', |
| | handler: () => selectRow(rowIndex), |
| | }, |
| | { |
| | text: '选中全部单元格', |
| | handler: selectAll, |
| | }, |
| | ] |
| | } |
| | </script> |
| | |
| | <style lang="scss" scoped> |
| | .editable-table { |
| | position: relative; |
| | user-select: none; |
| | } |
| | table { |
| | width: 100%; |
| | position: relative; |
| | table-layout: fixed; |
| | border-collapse: collapse; |
| | border-spacing: 0; |
| | border: 0; |
| | word-wrap: break-word; |
| | user-select: none; |
| | |
| | --themeColor: $themeColor; |
| | --subThemeColor1: $themeColor; |
| | --subThemeColor2: $themeColor; |
| | |
| | &.theme { |
| | background-color: #fff; |
| | |
| | tr:nth-child(2n) .cell { |
| | background-color: var(--subThemeColor1); |
| | } |
| | tr:nth-child(2n + 1) .cell { |
| | background-color: var(--subThemeColor2); |
| | } |
| | |
| | &.row-header { |
| | tr:first-child .cell { |
| | background-color: var(--themeColor); |
| | } |
| | } |
| | &.row-footer { |
| | tr:last-child .cell { |
| | background-color: var(--themeColor); |
| | } |
| | } |
| | &.col-header { |
| | tr .cell:first-child { |
| | background-color: var(--themeColor); |
| | } |
| | } |
| | &.col-footer { |
| | tr .cell:last-child { |
| | background-color: var(--themeColor); |
| | } |
| | } |
| | } |
| | |
| | .cell { |
| | position: relative; |
| | white-space: normal; |
| | word-wrap: break-word; |
| | vertical-align: middle; |
| | font-size: 14px; |
| | background-clip: padding-box; |
| | cursor: default; |
| | |
| | &.selected::after { |
| | content: ''; |
| | width: 100%; |
| | height: 100%; |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | background-color: rgba($color: #666, $alpha: .4); |
| | } |
| | } |
| | |
| | .cell-text { |
| | padding: 5px; |
| | line-height: 1.5; |
| | user-select: none; |
| | cursor: text; |
| | |
| | &.active { |
| | user-select: text; |
| | } |
| | } |
| | } |
| | |
| | .drag-line { |
| | position: absolute; |
| | top: 0; |
| | bottom: 0; |
| | width: 3px; |
| | background-color: $themeColor; |
| | margin-left: -1px; |
| | opacity: 0; |
| | z-index: 2; |
| | cursor: col-resize; |
| | } |
| | </style> |