mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
可编辑表格组件开发
This commit is contained in:
parent
d06c1068b1
commit
b2eefc80df
79
src/components/EditableDiv.vue
Normal file
79
src/components/EditableDiv.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="editable-div"
|
||||||
|
ref="editableDivRef"
|
||||||
|
:contenteditable="contenteditable"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@input="$event => handleInput($event)"
|
||||||
|
v-html="text"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'editable-div',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
contenteditable: {
|
||||||
|
type: [Boolean, String],
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const editableDivRef = ref<HTMLElement>()
|
||||||
|
const text = ref('')
|
||||||
|
const isFocus = ref(false)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, () => {
|
||||||
|
if(isFocus.value) return
|
||||||
|
text.value = props.modelValue
|
||||||
|
if(editableDivRef.value) editableDivRef.value.innerHTML = props.modelValue
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
if(!editableDivRef.value) return
|
||||||
|
const text = editableDivRef.value.innerHTML
|
||||||
|
emit('update:modelValue', text)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
isFocus.value = true
|
||||||
|
|
||||||
|
if(!editableDivRef.value) return
|
||||||
|
editableDivRef.value.onpaste = (e: ClipboardEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if(!e.clipboardData) return
|
||||||
|
|
||||||
|
const clipboardDataFirstItem = e.clipboardData.items[0]
|
||||||
|
|
||||||
|
if(clipboardDataFirstItem && clipboardDataFirstItem.kind === 'string' && clipboardDataFirstItem.type === 'text/plain') {
|
||||||
|
clipboardDataFirstItem.getAsString(text => emit('update:modelValue', text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
isFocus.value = false
|
||||||
|
if(editableDivRef.value) editableDivRef.value.onpaste = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if(editableDivRef.value) editableDivRef.value.onpaste = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
editableDivRef,
|
||||||
|
handleFocus,
|
||||||
|
handleInput,
|
||||||
|
handleBlur,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
@ -3,6 +3,17 @@
|
|||||||
class="editable-table"
|
class="editable-table"
|
||||||
:style="{ width: width + 'px' }"
|
:style="{ width: width + 'px' }"
|
||||||
>
|
>
|
||||||
|
<div class="handler">
|
||||||
|
<div
|
||||||
|
class="drag-line"
|
||||||
|
v-for="(pos, index) in dragLinePosition"
|
||||||
|
:key="index"
|
||||||
|
:style="{
|
||||||
|
left: pos + 'px',
|
||||||
|
}"
|
||||||
|
@mousedown="$event => handleMousedownColHandler($event, index)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col span="1" v-for="(width, index) in colWidths" :key="index" :width="width">
|
<col span="1" v-for="(width, index) in colWidths" :key="index" :width="width">
|
||||||
@ -28,10 +39,12 @@
|
|||||||
@mouseenter="handleCellMouseenter(rowIndex, colIndex)"
|
@mouseenter="handleCellMouseenter(rowIndex, colIndex)"
|
||||||
v-contextmenu="el => contextmenus(el)"
|
v-contextmenu="el => contextmenus(el)"
|
||||||
>
|
>
|
||||||
<div
|
<EditableDiv
|
||||||
class="cell-text"
|
class="cell-text"
|
||||||
|
:class="{ 'active': activedCell === `${rowIndex}_${colIndex}` }"
|
||||||
:contenteditable="activedCell === `${rowIndex}_${colIndex}` ? 'plaintext-only' : false"
|
:contenteditable="activedCell === `${rowIndex}_${colIndex}` ? 'plaintext-only' : false"
|
||||||
></div>
|
v-model="cell.text"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -40,9 +53,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createRandomCode } from '@/utils/common'
|
import { computed, defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
import { ContextmenuItem } from './Contextmenu/types'
|
import { ContextmenuItem } from './Contextmenu/types'
|
||||||
|
import { KEYS } from '@/configs/hotkey'
|
||||||
|
import { createRandomCode } from '@/utils/common'
|
||||||
|
|
||||||
|
import EditableDiv from './EditableDiv.vue'
|
||||||
|
|
||||||
interface TableCells {
|
interface TableCells {
|
||||||
id: string;
|
id: string;
|
||||||
@ -62,6 +78,9 @@ interface TableCells {
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'editable-table',
|
name: 'editable-table',
|
||||||
|
components: {
|
||||||
|
EditableDiv,
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const tableCells = ref<TableCells[][]>([
|
const tableCells = ref<TableCells[][]>([
|
||||||
[
|
[
|
||||||
@ -86,12 +105,22 @@ export default defineComponent({
|
|||||||
{ id: '15', colspan: 1, rowspan: 1, text: '' },
|
{ id: '15', colspan: 1, rowspan: 1, text: '' },
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
const width = 800
|
|
||||||
const colWidths = ref([160, 160, 160, 160, 160])
|
const colWidths = ref([160, 160, 160, 160, 160])
|
||||||
const isStartSelect = ref(false)
|
const isStartSelect = ref(false)
|
||||||
const startCell = ref<number[]>([])
|
const startCell = ref<number[]>([])
|
||||||
const endCell = ref<number[]>([])
|
const endCell = ref<number[]>([])
|
||||||
|
|
||||||
|
const width = computed(() => colWidths.value.reduce((a, b) => (a + b)))
|
||||||
|
|
||||||
|
const dragLinePosition = computed(() => {
|
||||||
|
const dragLinePosition: number[] = []
|
||||||
|
for(let i = 1; i < colWidths.value.length + 1; i++) {
|
||||||
|
const pos = colWidths.value.slice(0, i).reduce((a, b) => (a + b))
|
||||||
|
dragLinePosition.push(pos)
|
||||||
|
}
|
||||||
|
return dragLinePosition
|
||||||
|
})
|
||||||
|
|
||||||
const hideCells = computed(() => {
|
const hideCells = computed(() => {
|
||||||
const hideCells = []
|
const hideCells = []
|
||||||
|
|
||||||
@ -254,6 +283,7 @@ export default defineComponent({
|
|||||||
item.splice(colIndex, 1)
|
item.splice(colIndex, 1)
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
|
colWidths.value.splice(colIndex, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertRow = (selectedIndex: number, rowIndex: number) => {
|
const insertRow = (selectedIndex: number, rowIndex: number) => {
|
||||||
@ -281,6 +311,7 @@ export default defineComponent({
|
|||||||
item.splice(colIndex, 0, cell)
|
item.splice(colIndex, 0, cell)
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
|
colWidths.value.splice(colIndex, 0, 160)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeCells = () => {
|
const mergeCells = () => {
|
||||||
@ -310,6 +341,89 @@ export default defineComponent({
|
|||||||
removeSelectedCells()
|
removeSelectedCells()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMousedownColHandler = (e: MouseEvent, colIndex: number) => {
|
||||||
|
removeSelectedCells()
|
||||||
|
let isMouseDown = true
|
||||||
|
|
||||||
|
const originWidth = colWidths.value[colIndex]
|
||||||
|
const startPageX = e.pageX
|
||||||
|
|
||||||
|
const minWidth = 50
|
||||||
|
|
||||||
|
document.onmousemove = e => {
|
||||||
|
if(!isMouseDown) return
|
||||||
|
|
||||||
|
const moveX = e.pageX - startPageX
|
||||||
|
const width = originWidth + moveX < minWidth ? minWidth : Math.round(originWidth + moveX)
|
||||||
|
|
||||||
|
colWidths.value[colIndex] = width
|
||||||
|
}
|
||||||
|
document.onmouseup = () => {
|
||||||
|
isMouseDown = false
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSelectedCellText = () => {
|
||||||
|
const _tableCells: TableCells[][] = 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 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, nextRow + 1)
|
||||||
|
startCell.value = [nextRow + 1, 0]
|
||||||
|
}
|
||||||
|
else startCell.value = nextCell
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const textRef = document.querySelector('.cell-text.active') as HTMLInputElement
|
||||||
|
textRef.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const keydownListener = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toUpperCase()
|
||||||
|
if(selectedCells.value.length < 2) {
|
||||||
|
if(key === KEYS.TAB) {
|
||||||
|
e.preventDefault()
|
||||||
|
tabActiveCell()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(key === KEYS.DELETE) {
|
||||||
|
clearSelectedCellText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', keydownListener)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', keydownListener)
|
||||||
|
})
|
||||||
|
|
||||||
const getEffectiveTableCells = () => {
|
const getEffectiveTableCells = () => {
|
||||||
const effectiveTableCells = []
|
const effectiveTableCells = []
|
||||||
|
|
||||||
@ -390,6 +504,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
|
dragLinePosition,
|
||||||
tableCells,
|
tableCells,
|
||||||
colWidths,
|
colWidths,
|
||||||
hideCells,
|
hideCells,
|
||||||
@ -400,6 +515,7 @@ export default defineComponent({
|
|||||||
handleCellMouseenter,
|
handleCellMouseenter,
|
||||||
selectCol,
|
selectCol,
|
||||||
selectRow,
|
selectRow,
|
||||||
|
handleMousedownColHandler,
|
||||||
contextmenus,
|
contextmenus,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -409,6 +525,7 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.editable-table {
|
.editable-table {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -417,6 +534,7 @@ table {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -457,4 +575,20 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background-color: $themeColor;
|
||||||
|
margin-left: -1px;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: col-resize;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -84,8 +84,7 @@ export default () => {
|
|||||||
const DEFAULT_CELL_HEIGHT = 35
|
const DEFAULT_CELL_HEIGHT = 35
|
||||||
const DEFAULT_BORDER_WIDTH = 2
|
const DEFAULT_BORDER_WIDTH = 2
|
||||||
|
|
||||||
const colSizes: number[] = new Array(colCount).fill(DEFAULT_CELL_WIDTH)
|
const colWidths: number[] = new Array(colCount).fill(DEFAULT_CELL_WIDTH)
|
||||||
const rowSizes: number[] = new Array(rowCount).fill(DEFAULT_CELL_HEIGHT)
|
|
||||||
|
|
||||||
createElement({
|
createElement({
|
||||||
...DEFAULT_TABLE,
|
...DEFAULT_TABLE,
|
||||||
@ -93,8 +92,7 @@ export default () => {
|
|||||||
id: createRandomCode(),
|
id: createRandomCode(),
|
||||||
width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
|
width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
|
||||||
height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
|
height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
|
||||||
colSizes,
|
colWidths,
|
||||||
rowSizes,
|
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -151,10 +151,7 @@ export interface PPTTableElement {
|
|||||||
groupId?: string;
|
groupId?: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
borderTheme?: string;
|
colWidths: number[];
|
||||||
theme?: string;
|
|
||||||
rowSizes: number[];
|
|
||||||
colSizes: number[];
|
|
||||||
data: TableElementCell[][];
|
data: TableElementCell[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user