EditableTable.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. <template>
  2. <div
  3. class="editable-table"
  4. :style="{ width: totalWidth + 'px' }"
  5. >
  6. <div class="handler" v-if="editable">
  7. <div
  8. class="drag-line"
  9. v-for="(pos, index) in dragLinePosition"
  10. :key="index"
  11. :style="{
  12. left: pos + 'px',
  13. }"
  14. @mousedown="$event => handleMousedownColHandler($event, index)"
  15. ></div>
  16. </div>
  17. <table
  18. :class="{
  19. 'theme': theme,
  20. 'row-header': theme?.rowHeader,
  21. 'row-footer': theme?.rowFooter,
  22. 'col-header': theme?.colHeader,
  23. 'col-footer': theme?.colFooter,
  24. }"
  25. :style="`--themeColor: ${theme?.color}; --subThemeColor1: ${subThemeColor[0]}; --subThemeColor2: ${subThemeColor[1]}`"
  26. >
  27. <colgroup>
  28. <col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
  29. </colgroup>
  30. <tbody>
  31. <tr
  32. v-for="(rowCells, rowIndex) in tableCells"
  33. :key="rowIndex"
  34. >
  35. <td
  36. class="cell"
  37. :class="{
  38. 'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,
  39. 'active': activedCell === `${rowIndex}_${colIndex}`,
  40. }"
  41. :style="{
  42. borderStyle: outline.style,
  43. borderColor: outline.color,
  44. borderWidth: outline.width + 'px',
  45. ...getTextStyle(cell.style),
  46. }"
  47. v-for="(cell, colIndex) in rowCells"
  48. :key="cell.id"
  49. :rowspan="cell.rowspan"
  50. :colspan="cell.colspan"
  51. :data-cell-index="`${rowIndex}_${colIndex}`"
  52. v-show="!hideCells.includes(`${rowIndex}_${colIndex}`)"
  53. @mousedown="$event => handleCellMousedown($event, rowIndex, colIndex)"
  54. @mouseenter="handleCellMouseenter(rowIndex, colIndex)"
  55. v-contextmenu="el => contextmenus(el)"
  56. >
  57. <CustomTextarea
  58. class="cell-text"
  59. :class="{ 'active': activedCell === `${rowIndex}_${colIndex}` }"
  60. :contenteditable="activedCell === `${rowIndex}_${colIndex}` ? 'plaintext-only' : false"
  61. v-model="cell.text"
  62. @update:modelValue="handleInput()"
  63. />
  64. </td>
  65. </tr>
  66. </tbody>
  67. </table>
  68. </div>
  69. </template>
  70. <script lang="ts">
  71. import { computed, defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
  72. import debounce from 'lodash/debounce'
  73. import tinycolor from 'tinycolor2'
  74. import { useStore } from '@/store'
  75. import { PPTElementOutline, TableCell, TableCellStyle, TableTheme } from '@/types/slides'
  76. import { ContextmenuItem } from '@/components/Contextmenu/types'
  77. import { KEYS } from '@/configs/hotkey'
  78. import { createRandomCode } from '@/utils/common'
  79. import CustomTextarea from './CustomTextarea.vue'
  80. export default defineComponent({
  81. name: 'editable-table',
  82. components: {
  83. CustomTextarea,
  84. },
  85. props: {
  86. data: {
  87. type: Array as PropType<TableCell[][]>,
  88. required: true,
  89. },
  90. width: {
  91. type: Number,
  92. required: true,
  93. },
  94. colWidths: {
  95. type: Array as PropType<number[]>,
  96. required: true,
  97. },
  98. outline: {
  99. type: Object as PropType<PPTElementOutline>,
  100. required: true,
  101. },
  102. theme: {
  103. type: Object as PropType<TableTheme>,
  104. },
  105. editable: {
  106. type: Boolean,
  107. default: true,
  108. },
  109. },
  110. setup(props, { emit }) {
  111. const store = useStore()
  112. const canvasScale = computed(() => store.state.canvasScale)
  113. const subThemeColor = ref(['', ''])
  114. watch(() => props.theme, () => {
  115. if (props.theme) {
  116. const rgba = tinycolor(props.theme.color).toRgb()
  117. const subRgba1 = { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a * 0.3 }
  118. const subRgba2 = { r: rgba.r, g: rgba.g, b: rgba.b, a: rgba.a * 0.1 }
  119. subThemeColor.value = [
  120. `rgba(${[subRgba1.r, subRgba1.g, subRgba1.b, subRgba1.a].join(',')})`,
  121. `rgba(${[subRgba2.r, subRgba2.g, subRgba2.b, subRgba2.a].join(',')})`,
  122. ]
  123. }
  124. }, { immediate: true })
  125. const tableCells = computed<TableCell[][]>({
  126. get() {
  127. return props.data
  128. },
  129. set(newData) {
  130. emit('change', newData)
  131. },
  132. })
  133. const colSizeList = ref<number[]>([])
  134. const totalWidth = computed(() => colSizeList.value.reduce((a, b) => a + b))
  135. watch([
  136. () => props.colWidths,
  137. () => props.width,
  138. ], () => {
  139. colSizeList.value = props.colWidths.map(item => item * props.width)
  140. }, { immediate: true })
  141. const isStartSelect = ref(false)
  142. const startCell = ref<number[]>([])
  143. const endCell = ref<number[]>([])
  144. const removeSelectedCells = () => {
  145. startCell.value = []
  146. endCell.value = []
  147. }
  148. watch(() => props.editable, () => {
  149. if (!props.editable) removeSelectedCells()
  150. })
  151. const dragLinePosition = computed(() => {
  152. const dragLinePosition: number[] = []
  153. for (let i = 1; i < colSizeList.value.length + 1; i++) {
  154. const pos = colSizeList.value.slice(0, i).reduce((a, b) => (a + b))
  155. dragLinePosition.push(pos)
  156. }
  157. return dragLinePosition
  158. })
  159. const hideCells = computed(() => {
  160. const hideCells = []
  161. for (let i = 0; i < tableCells.value.length; i++) {
  162. const rowCells = tableCells.value[i]
  163. for (let j = 0; j < rowCells.length; j++) {
  164. const cell = rowCells[j]
  165. if (cell.colspan > 1 || cell.rowspan > 1) {
  166. for (let row = i; row < i + cell.rowspan; row++) {
  167. for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) {
  168. hideCells.push(`${row}_${col}`)
  169. }
  170. }
  171. }
  172. }
  173. }
  174. return hideCells
  175. })
  176. const selectedCells = computed(() => {
  177. if (!startCell.value.length) return []
  178. const [startX, startY] = startCell.value
  179. if (!endCell.value.length) return [`${startX}_${startY}`]
  180. const [endX, endY] = endCell.value
  181. if (startX === endX && startY === endY) return [`${startX}_${startY}`]
  182. const selectedCells = []
  183. const minX = Math.min(startX, endX)
  184. const minY = Math.min(startY, endY)
  185. const maxX = Math.max(startX, endX)
  186. const maxY = Math.max(startY, endY)
  187. for (let i = 0; i < tableCells.value.length; i++) {
  188. const rowCells = tableCells.value[i]
  189. for (let j = 0; j < rowCells.length; j++) {
  190. if (i >= minX && i <= maxX && j >= minY && j <= maxY) selectedCells.push(`${i}_${j}`)
  191. }
  192. }
  193. return selectedCells
  194. })
  195. watch(selectedCells, () => {
  196. emit('changeSelectedCells', selectedCells.value)
  197. })
  198. const activedCell = computed(() => {
  199. if (selectedCells.value.length > 1) return null
  200. return selectedCells.value[0]
  201. })
  202. const selectedRange = computed(() => {
  203. if (!startCell.value.length) return null
  204. const [startX, startY] = startCell.value
  205. if (!endCell.value.length) return { row: [startX, startX], col: [startY, startY] }
  206. const [endX, endY] = endCell.value
  207. if (startX === endX && startY === endY) return { row: [startX, startX], col: [startY, startY] }
  208. const minX = Math.min(startX, endX)
  209. const minY = Math.min(startY, endY)
  210. const maxX = Math.max(startX, endX)
  211. const maxY = Math.max(startY, endY)
  212. return {
  213. row: [minX, maxX],
  214. col: [minY, maxY],
  215. }
  216. })
  217. const handleMouseup = () => isStartSelect.value = false
  218. const handleCellMousedown = (e: MouseEvent, rowIndex: number, colIndex: number) => {
  219. if (e.button === 0) {
  220. endCell.value = []
  221. isStartSelect.value = true
  222. startCell.value = [rowIndex, colIndex]
  223. }
  224. }
  225. const handleCellMouseenter = (rowIndex: number, colIndex: number) => {
  226. if (!isStartSelect.value) return
  227. endCell.value = [rowIndex, colIndex]
  228. }
  229. onMounted(() => {
  230. document.addEventListener('mouseup', handleMouseup)
  231. })
  232. onUnmounted(() => {
  233. document.removeEventListener('mouseup', handleMouseup)
  234. })
  235. const isHideCell = (rowIndex: number, colIndex: number) => hideCells.value.includes(`${rowIndex}_${colIndex}`)
  236. const selectCol = (index: number) => {
  237. const maxRow = tableCells.value.length - 1
  238. startCell.value = [0, index]
  239. endCell.value = [maxRow, index]
  240. }
  241. const selectRow = (index: number) => {
  242. const maxCol = tableCells.value[index].length - 1
  243. startCell.value = [index, 0]
  244. endCell.value = [index, maxCol]
  245. }
  246. const selectAll = () => {
  247. const maxRow = tableCells.value.length - 1
  248. const maxCol = tableCells.value[maxRow].length - 1
  249. startCell.value = [0, 0]
  250. endCell.value = [maxRow, maxCol]
  251. }
  252. const deleteRow = (rowIndex: number) => {
  253. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  254. const targetCells = tableCells.value[rowIndex]
  255. const hideCellsPos = []
  256. for (let i = 0; i < targetCells.length; i++) {
  257. if (isHideCell(rowIndex, i)) hideCellsPos.push(i)
  258. }
  259. for (const pos of hideCellsPos) {
  260. for (let i = rowIndex; i >= 0; i--) {
  261. if (!isHideCell(i, pos)) {
  262. _tableCells[i][pos].rowspan = _tableCells[i][pos].rowspan - 1
  263. break
  264. }
  265. }
  266. }
  267. _tableCells.splice(rowIndex, 1)
  268. tableCells.value = _tableCells
  269. }
  270. const deleteCol = (colIndex: number) => {
  271. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  272. const hideCellsPos = []
  273. for (let i = 0; i < tableCells.value.length; i++) {
  274. if (isHideCell(i, colIndex)) hideCellsPos.push(i)
  275. }
  276. for (const pos of hideCellsPos) {
  277. for (let i = colIndex; i >= 0; i--) {
  278. if (!isHideCell(pos, i)) {
  279. _tableCells[pos][i].colspan = _tableCells[pos][i].colspan - 1
  280. break
  281. }
  282. }
  283. }
  284. tableCells.value = _tableCells.map(item => {
  285. item.splice(colIndex, 1)
  286. return item
  287. })
  288. colSizeList.value.splice(colIndex, 1)
  289. emit('changeColWidths', colSizeList.value)
  290. }
  291. const insertRow = (rowIndex: number) => {
  292. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  293. const rowCells: TableCell[] = []
  294. for (let i = 0; i < _tableCells[0].length; i++) {
  295. rowCells.push({
  296. colspan: 1,
  297. rowspan: 1,
  298. text: '',
  299. id: createRandomCode(),
  300. })
  301. }
  302. _tableCells.splice(rowIndex, 0, rowCells)
  303. tableCells.value = _tableCells
  304. }
  305. const insertCol = (colIndex: number) => {
  306. tableCells.value = tableCells.value.map(item => {
  307. const cell = {
  308. colspan: 1,
  309. rowspan: 1,
  310. text: '',
  311. id: createRandomCode(),
  312. }
  313. item.splice(colIndex, 0, cell)
  314. return item
  315. })
  316. colSizeList.value.splice(colIndex, 0, 100)
  317. emit('changeColWidths', colSizeList.value)
  318. }
  319. const mergeCells = () => {
  320. const [startX, startY] = startCell.value
  321. const [endX, endY] = endCell.value
  322. const minX = Math.min(startX, endX)
  323. const minY = Math.min(startY, endY)
  324. const maxX = Math.max(startX, endX)
  325. const maxY = Math.max(startY, endY)
  326. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  327. _tableCells[minX][minY].rowspan = maxX - minX + 1
  328. _tableCells[minX][minY].colspan = maxY - minY + 1
  329. tableCells.value = _tableCells
  330. removeSelectedCells()
  331. }
  332. const splitCells = (rowIndex: number, colIndex: number) => {
  333. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  334. _tableCells[rowIndex][colIndex].rowspan = 1
  335. _tableCells[rowIndex][colIndex].colspan = 1
  336. tableCells.value = _tableCells
  337. removeSelectedCells()
  338. }
  339. const handleMousedownColHandler = (e: MouseEvent, colIndex: number) => {
  340. removeSelectedCells()
  341. let isMouseDown = true
  342. const originWidth = colSizeList.value[colIndex]
  343. const startPageX = e.pageX
  344. const minWidth = 50
  345. document.onmousemove = e => {
  346. if (!isMouseDown) return
  347. const moveX = (e.pageX - startPageX) / canvasScale.value
  348. const width = originWidth + moveX < minWidth ? minWidth : Math.round(originWidth + moveX)
  349. colSizeList.value[colIndex] = width
  350. }
  351. document.onmouseup = () => {
  352. isMouseDown = false
  353. document.onmousemove = null
  354. document.onmouseup = null
  355. emit('changeColWidths', colSizeList.value)
  356. }
  357. }
  358. const clearSelectedCellText = () => {
  359. const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
  360. for (let i = 0; i < _tableCells.length; i++) {
  361. for (let j = 0; j < _tableCells[i].length; j++) {
  362. if (selectedCells.value.includes(`${i}_${j}`)) {
  363. _tableCells[i][j].text = ''
  364. }
  365. }
  366. }
  367. tableCells.value = _tableCells
  368. }
  369. const tabActiveCell = () => {
  370. const getNextCell = (i: number, j: number): [number, number] | null => {
  371. if (!tableCells.value[i]) return null
  372. if (!tableCells.value[i][j]) return getNextCell(i + 1, 0)
  373. if (isHideCell(i, j)) return getNextCell(i, j + 1)
  374. return [i, j]
  375. }
  376. endCell.value = []
  377. const nextRow = startCell.value[0]
  378. const nextCol = startCell.value[1] + 1
  379. const nextCell = getNextCell(nextRow, nextCol)
  380. if (!nextCell) {
  381. insertRow(nextRow + 1)
  382. startCell.value = [nextRow + 1, 0]
  383. }
  384. else startCell.value = nextCell
  385. nextTick(() => {
  386. const textRef = document.querySelector('.cell-text.active') as HTMLInputElement
  387. if (textRef) textRef.focus()
  388. })
  389. }
  390. const keydownListener = (e: KeyboardEvent) => {
  391. if (!props.editable || !selectedCells.value.length) return
  392. const key = e.key.toUpperCase()
  393. if (selectedCells.value.length < 2) {
  394. if (key === KEYS.TAB) {
  395. e.preventDefault()
  396. tabActiveCell()
  397. }
  398. if (e.ctrlKey && key === KEYS.UP) {
  399. e.preventDefault()
  400. const rowIndex = +selectedCells.value[0].split('_')[0]
  401. insertRow(rowIndex)
  402. }
  403. if (e.ctrlKey && key === KEYS.DOWN) {
  404. e.preventDefault()
  405. const rowIndex = +selectedCells.value[0].split('_')[0]
  406. insertRow(rowIndex + 1)
  407. }
  408. if (e.ctrlKey && key === KEYS.LEFT) {
  409. e.preventDefault()
  410. const colIndex = +selectedCells.value[0].split('_')[1]
  411. insertCol(colIndex)
  412. }
  413. if (e.ctrlKey && key === KEYS.RIGHT) {
  414. e.preventDefault()
  415. const colIndex = +selectedCells.value[0].split('_')[1]
  416. insertCol(colIndex + 1)
  417. }
  418. }
  419. else if (key === KEYS.DELETE) {
  420. clearSelectedCellText()
  421. }
  422. }
  423. onMounted(() => {
  424. document.addEventListener('keydown', keydownListener)
  425. })
  426. onUnmounted(() => {
  427. document.removeEventListener('keydown', keydownListener)
  428. })
  429. const getTextStyle = (style?: TableCellStyle) => {
  430. if (!style) return {}
  431. const {
  432. bold,
  433. em,
  434. underline,
  435. strikethrough,
  436. color,
  437. backcolor,
  438. fontsize,
  439. fontname,
  440. align,
  441. } = style
  442. return {
  443. fontWeight: bold ? 'bold' : 'normal',
  444. fontStyle: em ? 'italic' : 'normal',
  445. textDecoration: `${underline ? 'underline' : ''} ${strikethrough ? 'line-through' : ''}`,
  446. color: color || '#000',
  447. backgroundColor: backcolor || '',
  448. fontSize: fontsize || '14px',
  449. fontFamily: fontname || '微软雅黑',
  450. textAlign: align || 'left',
  451. }
  452. }
  453. const handleInput = debounce(function () {
  454. emit('change', tableCells.value)
  455. }, 300, { trailing: true })
  456. const getEffectiveTableCells = () => {
  457. const effectiveTableCells = []
  458. for (let i = 0; i < tableCells.value.length; i++) {
  459. const rowCells = tableCells.value[i]
  460. const _rowCells = []
  461. for (let j = 0; j < rowCells.length; j++) {
  462. if (!isHideCell(i, j)) _rowCells.push(rowCells[j])
  463. }
  464. if (_rowCells.length) effectiveTableCells.push(_rowCells)
  465. }
  466. return effectiveTableCells
  467. }
  468. const checkCanDeleteRowOrCol = () => {
  469. const effectiveTableCells = getEffectiveTableCells()
  470. const canDeleteRow = effectiveTableCells.length > 1
  471. const canDeleteCol = effectiveTableCells[0].length > 1
  472. return { canDeleteRow, canDeleteCol }
  473. }
  474. const checkCanMergeOrSplit = (rowIndex: number, colIndex: number) => {
  475. const isMultiSelected = selectedCells.value.length > 1
  476. const targetCell = tableCells.value[rowIndex][colIndex]
  477. const canMerge = isMultiSelected
  478. const canSplit = !isMultiSelected && (targetCell.rowspan > 1 || targetCell.colspan > 1)
  479. return { canMerge, canSplit }
  480. }
  481. const contextmenus = (el: HTMLElement): ContextmenuItem[] => {
  482. const cellIndex = el.dataset.cellIndex as string
  483. const rowIndex = +cellIndex.split('_')[0]
  484. const colIndex = +cellIndex.split('_')[1]
  485. if (!selectedCells.value.includes(`${rowIndex}_${colIndex}`)) {
  486. startCell.value = [rowIndex, colIndex]
  487. endCell.value = []
  488. }
  489. const { canMerge, canSplit } = checkCanMergeOrSplit(rowIndex, colIndex)
  490. const { canDeleteRow, canDeleteCol } = checkCanDeleteRowOrCol()
  491. return [
  492. {
  493. text: '插入列',
  494. children: [
  495. { text: '到左侧', handler: () => insertCol(colIndex) },
  496. { text: '到右侧', handler: () => insertCol(colIndex + 1) },
  497. ],
  498. },
  499. {
  500. text: '插入行',
  501. children: [
  502. { text: '到上方', handler: () => insertRow(rowIndex) },
  503. { text: '到下方', handler: () => insertRow(rowIndex + 1) },
  504. ],
  505. },
  506. {
  507. text: '删除列',
  508. disable: !canDeleteCol,
  509. handler: () => deleteCol(colIndex),
  510. },
  511. {
  512. text: '删除行',
  513. disable: !canDeleteRow,
  514. handler: () => deleteRow(rowIndex),
  515. },
  516. { divider: true },
  517. {
  518. text: '合并单元格',
  519. disable: !canMerge,
  520. handler: mergeCells,
  521. },
  522. {
  523. text: '取消合并单元格',
  524. disable: !canSplit,
  525. handler: () => splitCells(rowIndex, colIndex),
  526. },
  527. { divider: true },
  528. {
  529. text: '选中当前列',
  530. handler: () => selectCol(colIndex),
  531. },
  532. {
  533. text: '选中当前行',
  534. handler: () => selectRow(rowIndex),
  535. },
  536. {
  537. text: '选中全部单元格',
  538. handler: selectAll,
  539. },
  540. ]
  541. }
  542. return {
  543. getTextStyle,
  544. dragLinePosition,
  545. tableCells,
  546. colSizeList,
  547. totalWidth,
  548. hideCells,
  549. selectedCells,
  550. activedCell,
  551. selectedRange,
  552. handleCellMousedown,
  553. handleCellMouseenter,
  554. selectCol,
  555. selectRow,
  556. handleMousedownColHandler,
  557. contextmenus,
  558. handleInput,
  559. subThemeColor,
  560. }
  561. },
  562. })
  563. </script>
  564. <style lang="scss" scoped>
  565. .editable-table {
  566. position: relative;
  567. user-select: none;
  568. }
  569. table {
  570. width: 100%;
  571. position: relative;
  572. table-layout: fixed;
  573. border-collapse: collapse;
  574. border-spacing: 0;
  575. border: 0;
  576. word-wrap: break-word;
  577. user-select: none;
  578. --themeColor: $themeColor;
  579. --subThemeColor1: $themeColor;
  580. --subThemeColor2: $themeColor;
  581. &.theme {
  582. tr:nth-child(2n) .cell {
  583. background-color: var(--subThemeColor1);
  584. }
  585. tr:nth-child(2n + 1) .cell {
  586. background-color: var(--subThemeColor2);
  587. }
  588. &.row-header {
  589. tr:first-child .cell {
  590. background-color: var(--themeColor);
  591. }
  592. }
  593. &.row-footer {
  594. tr:last-child .cell {
  595. background-color: var(--themeColor);
  596. }
  597. }
  598. &.col-header {
  599. tr .cell:first-child {
  600. background-color: var(--themeColor);
  601. }
  602. }
  603. &.col-footer {
  604. tr .cell:last-child {
  605. background-color: var(--themeColor);
  606. }
  607. }
  608. }
  609. tr {
  610. height: 36px;
  611. }
  612. .cell {
  613. position: relative;
  614. white-space: normal;
  615. word-wrap: break-word;
  616. vertical-align: middle;
  617. font-size: 14px;
  618. cursor: default;
  619. &.selected::after {
  620. content: '';
  621. width: 100%;
  622. height: 100%;
  623. position: absolute;
  624. top: 0;
  625. left: 0;
  626. background-color: rgba($color: $themeColor, $alpha: .3);
  627. }
  628. }
  629. .cell-text {
  630. min-height: 32px;
  631. padding: 5px;
  632. border: 0;
  633. outline: 0;
  634. line-height: 1.5;
  635. user-select: none;
  636. cursor: text;
  637. &.active {
  638. user-select: text;
  639. }
  640. }
  641. }
  642. .drag-line {
  643. position: absolute;
  644. top: 0;
  645. bottom: 0;
  646. width: 3px;
  647. background-color: $themeColor;
  648. margin-left: -1px;
  649. opacity: 0;
  650. z-index: 2;
  651. cursor: col-resize;
  652. &:hover {
  653. opacity: 1;
  654. }
  655. }
  656. </style>