mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
完成基础表格元素
This commit is contained in:
parent
e3f20147db
commit
a1515e1994
@ -3,7 +3,7 @@ import { MutationTypes } from '@/store'
|
||||
import { createRandomCode } from '@/utils/common'
|
||||
import { getImageSize } from '@/utils/image'
|
||||
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||
import { ChartType, PPTElement, TableCell } from '@/types/slides'
|
||||
import { ChartType, PPTElement } from '@/types/slides'
|
||||
import { ShapePoolItem } from '@/configs/shapes'
|
||||
import { LinePoolItem } from '@/configs/lines'
|
||||
import {
|
||||
@ -12,7 +12,6 @@ import {
|
||||
DEFAULT_SHAPE,
|
||||
DEFAULT_LINE,
|
||||
DEFAULT_CHART,
|
||||
DEFAULT_TABLE,
|
||||
} from '@/configs/element'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
@ -77,24 +76,7 @@ export default () => {
|
||||
}
|
||||
|
||||
const createTableElement = (rowCount: number, colCount: number) => {
|
||||
const row: TableCell[] = new Array(colCount).fill({ colspan: 1, rowspan: 1, content: '' })
|
||||
const data: TableCell[][] = new Array(rowCount).fill(row)
|
||||
|
||||
const DEFAULT_CELL_WIDTH = 80
|
||||
const DEFAULT_CELL_HEIGHT = 35
|
||||
const DEFAULT_BORDER_WIDTH = 2
|
||||
|
||||
const colWidths: number[] = new Array(colCount).fill(DEFAULT_CELL_WIDTH)
|
||||
|
||||
createElement({
|
||||
...DEFAULT_TABLE,
|
||||
type: 'table',
|
||||
id: createRandomCode(),
|
||||
width: colCount * DEFAULT_CELL_WIDTH + DEFAULT_BORDER_WIDTH,
|
||||
height: rowCount * DEFAULT_CELL_HEIGHT + DEFAULT_BORDER_WIDTH,
|
||||
colWidths,
|
||||
data,
|
||||
})
|
||||
console.log(rowCount, colCount)
|
||||
}
|
||||
|
||||
const createTextElement = (position: CommonElementPosition) => {
|
||||
|
@ -25,8 +25,13 @@ export const slides: Slide[] = [
|
||||
left: 20,
|
||||
top: 20,
|
||||
width: 400,
|
||||
height: 90,
|
||||
height: 108,
|
||||
colWidths: [0.25, 0.25, 0.25, 0.25],
|
||||
outline: {
|
||||
width: 1,
|
||||
style: 'solid',
|
||||
color: '#999',
|
||||
},
|
||||
data: [
|
||||
[
|
||||
{ id: '1', colspan: 1, rowspan: 1, text: '' },
|
||||
|
@ -160,6 +160,7 @@ export interface PPTTableElement {
|
||||
groupId?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
outline: PPTElementOutline;
|
||||
colWidths: number[];
|
||||
data: TableCell[][];
|
||||
}
|
||||
|
@ -61,7 +61,9 @@ export default defineComponent({
|
||||
const store = useStore<State>()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
|
||||
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
|
||||
const outlineWidth = computed(() => props.elementInfo.outline.width || 1)
|
||||
|
||||
const scaleWidth = computed(() => (props.elementInfo.width + outlineWidth.value) * canvasScale.value)
|
||||
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
|
||||
|
||||
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
|
||||
|
@ -24,6 +24,7 @@ import BaseTextElement from '@/views/components/element/TextElement/BaseTextElem
|
||||
import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
|
||||
import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
|
||||
import ScreenChartElement from '@/views/components/element/ChartElement/ScreenChartElement.vue'
|
||||
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'screen-element',
|
||||
@ -49,6 +50,7 @@ export default defineComponent({
|
||||
[ElementTypes.SHAPE]: BaseShapeElement,
|
||||
[ElementTypes.LINE]: BaseLineElement,
|
||||
[ElementTypes.CHART]: ScreenChartElement,
|
||||
[ElementTypes.TABLE]: BaseTableElement,
|
||||
}
|
||||
return elementTypeMap[props.elementInfo.type] || null
|
||||
})
|
||||
|
@ -20,6 +20,7 @@ import BaseTextElement from '@/views/components/element/TextElement/BaseTextElem
|
||||
import BaseShapeElement from '@/views/components/element/ShapeElement/BaseShapeElement.vue'
|
||||
import BaseLineElement from '@/views/components/element/LineElement/BaseLineElement.vue'
|
||||
import BaseChartElement from '@/views/components/element/ChartElement/BaseChartElement.vue'
|
||||
import BaseTableElement from '@/views/components/element/TableElement/BaseTableElement.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'base-element',
|
||||
@ -41,6 +42,7 @@ export default defineComponent({
|
||||
[ElementTypes.SHAPE]: BaseShapeElement,
|
||||
[ElementTypes.LINE]: BaseLineElement,
|
||||
[ElementTypes.CHART]: BaseChartElement,
|
||||
[ElementTypes.TABLE]: BaseTableElement,
|
||||
}
|
||||
return elementTypeMap[props.elementInfo.type] || null
|
||||
})
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="base-element-shape"
|
||||
<div
|
||||
class="base-element-shape"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="editable-element-shape"
|
||||
<div
|
||||
class="editable-element-shape"
|
||||
:class="{ 'lock': elementInfo.lock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
|
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-element-table"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
width: elementInfo.width + 'px',
|
||||
}"
|
||||
>
|
||||
<div class="element-content">
|
||||
<StaticTable
|
||||
:data="elementInfo.data"
|
||||
:width="elementInfo.width"
|
||||
:colWidths="elementInfo.colWidths"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { PPTTableElement } from '@/types/slides'
|
||||
|
||||
import StaticTable from './StaticTable.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'base-element-table',
|
||||
components: {
|
||||
StaticTable,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTTableElement>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.base-element-table {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.element-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="editable-div"
|
||||
ref="editableDivRef"
|
||||
class="custom-textarea"
|
||||
ref="textareaRef"
|
||||
:contenteditable="contenteditable"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@ -14,7 +14,7 @@
|
||||
import { defineComponent, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-div',
|
||||
name: 'custom-textarea',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
@ -26,27 +26,27 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const editableDivRef = ref<HTMLElement>()
|
||||
const textareaRef = 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
|
||||
if(textareaRef.value) textareaRef.value.innerHTML = props.modelValue
|
||||
}, { immediate: true })
|
||||
|
||||
const handleInput = () => {
|
||||
if(!editableDivRef.value) return
|
||||
const text = editableDivRef.value.innerHTML
|
||||
if(!textareaRef.value) return
|
||||
const text = textareaRef.value.innerHTML
|
||||
emit('update:modelValue', text)
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
isFocus.value = true
|
||||
|
||||
if(!editableDivRef.value) return
|
||||
editableDivRef.value.onpaste = (e: ClipboardEvent) => {
|
||||
if(!textareaRef.value) return
|
||||
textareaRef.value.onpaste = (e: ClipboardEvent) => {
|
||||
e.preventDefault()
|
||||
if(!e.clipboardData) return
|
||||
|
||||
@ -60,15 +60,15 @@ export default defineComponent({
|
||||
|
||||
const handleBlur = () => {
|
||||
isFocus.value = false
|
||||
if(editableDivRef.value) editableDivRef.value.onpaste = null
|
||||
if(textareaRef.value) textareaRef.value.onpaste = null
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if(editableDivRef.value) editableDivRef.value.onpaste = null
|
||||
if(textareaRef.value) textareaRef.value.onpaste = null
|
||||
})
|
||||
|
||||
return {
|
||||
editableDivRef,
|
||||
textareaRef,
|
||||
handleFocus,
|
||||
handleInput,
|
||||
handleBlur,
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="editable-table"
|
||||
:style="{ width: width + 'px' }"
|
||||
:style="{ width: totalWidth + 'px' }"
|
||||
>
|
||||
<div class="handler">
|
||||
<div class="handler" v-if="editable">
|
||||
<div
|
||||
class="drag-line"
|
||||
v-for="(pos, index) in dragLinePosition"
|
||||
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col span="1" v-for="(width, index) in colWidths" :key="index" :width="width">
|
||||
<col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr
|
||||
@ -29,6 +29,11 @@
|
||||
'selected': selectedCells.includes(`${rowIndex}_${colIndex}`) && selectedCells.length > 1,
|
||||
'active': activedCell === `${rowIndex}_${colIndex}`,
|
||||
}"
|
||||
:style="{
|
||||
borderStyle: outline.style,
|
||||
borderColor: outline.color,
|
||||
borderWidth: outline.width + 'px',
|
||||
}"
|
||||
v-for="(cell, colIndex) in rowCells"
|
||||
:key="cell.id"
|
||||
:rowspan="cell.rowspan"
|
||||
@ -39,7 +44,7 @@
|
||||
@mouseenter="handleCellMouseenter(rowIndex, colIndex)"
|
||||
v-contextmenu="el => contextmenus(el)"
|
||||
>
|
||||
<EditableDiv
|
||||
<CustomTextarea
|
||||
class="cell-text"
|
||||
:class="{ 'active': activedCell === `${rowIndex}_${colIndex}` }"
|
||||
:contenteditable="activedCell === `${rowIndex}_${colIndex}` ? 'plaintext-only' : false"
|
||||
@ -54,39 +59,48 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, nextTick, onMounted, onUnmounted, PropType, ref } from 'vue'
|
||||
import { computed, defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { TableCell } from '@/types/slides'
|
||||
import { PPTElementOutline, TableCell } from '@/types/slides'
|
||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import { KEYS } from '@/configs/hotkey'
|
||||
import { createRandomCode } from '@/utils/common'
|
||||
|
||||
import EditableDiv from './EditableDiv.vue'
|
||||
import CustomTextarea from './CustomTextarea.vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State } from '@/store'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-table',
|
||||
components: {
|
||||
EditableDiv,
|
||||
CustomTextarea,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<TableCell[][]>,
|
||||
required: true,
|
||||
},
|
||||
outlineWidth: {
|
||||
width: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true,
|
||||
},
|
||||
outlineColor: {
|
||||
type: String,
|
||||
default: '#41464b',
|
||||
colWidths: {
|
||||
type: Array as PropType<number[]>,
|
||||
required: true,
|
||||
},
|
||||
outlineStyle: {
|
||||
type: String,
|
||||
default: 'solid',
|
||||
outline: {
|
||||
type: Object as PropType<PPTElementOutline>,
|
||||
required: true,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const store = useStore<State>()
|
||||
const canvasScale = computed(() => store.state.canvasScale)
|
||||
|
||||
const tableCells = computed<TableCell[][]>({
|
||||
get() {
|
||||
return props.data
|
||||
@ -95,18 +109,33 @@ export default defineComponent({
|
||||
emit('change', newData)
|
||||
},
|
||||
})
|
||||
|
||||
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 colWidths = ref([160, 160, 160, 160, 160])
|
||||
const isStartSelect = ref(false)
|
||||
const startCell = ref<number[]>([])
|
||||
const endCell = ref<number[]>([])
|
||||
|
||||
const removeSelectedCells = () => {
|
||||
startCell.value = []
|
||||
endCell.value = []
|
||||
}
|
||||
|
||||
const width = computed(() => colWidths.value.reduce((a, b) => (a + b)))
|
||||
watch(() => props.editable, () => {
|
||||
if(!props.editable) removeSelectedCells()
|
||||
})
|
||||
|
||||
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))
|
||||
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
|
||||
@ -207,11 +236,6 @@ export default defineComponent({
|
||||
|
||||
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]
|
||||
@ -274,12 +298,15 @@ export default defineComponent({
|
||||
item.splice(colIndex, 1)
|
||||
return item
|
||||
})
|
||||
colWidths.value.splice(colIndex, 1)
|
||||
colSizeList.value.splice(colIndex, 1)
|
||||
emit('changeColWidths', colSizeList.value)
|
||||
}
|
||||
|
||||
const insertRow = (selectedIndex: number, rowIndex: number) => {
|
||||
const insertRow = (rowIndex: number) => {
|
||||
const _tableCells: TableCell[][] = JSON.parse(JSON.stringify(tableCells.value))
|
||||
|
||||
const rowCells: TableCell[] = []
|
||||
for(let i = 0; i < tableCells.value[0].length; i++) {
|
||||
for(let i = 0; i < _tableCells[0].length; i++) {
|
||||
rowCells.push({
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
@ -288,10 +315,11 @@ export default defineComponent({
|
||||
})
|
||||
}
|
||||
|
||||
tableCells.value.splice(rowIndex, 0, rowCells)
|
||||
_tableCells.splice(rowIndex, 0, rowCells)
|
||||
tableCells.value = _tableCells
|
||||
}
|
||||
|
||||
const insertCol = (selectedIndex: number, colIndex: number) => {
|
||||
const insertCol = (colIndex: number) => {
|
||||
tableCells.value = tableCells.value.map(item => {
|
||||
const cell = {
|
||||
colspan: 1,
|
||||
@ -302,7 +330,8 @@ export default defineComponent({
|
||||
item.splice(colIndex, 0, cell)
|
||||
return item
|
||||
})
|
||||
colWidths.value.splice(colIndex, 0, 160)
|
||||
colSizeList.value.splice(colIndex, 0, 100)
|
||||
emit('changeColWidths', colSizeList.value)
|
||||
}
|
||||
|
||||
const mergeCells = () => {
|
||||
@ -336,7 +365,7 @@ export default defineComponent({
|
||||
removeSelectedCells()
|
||||
let isMouseDown = true
|
||||
|
||||
const originWidth = colWidths.value[colIndex]
|
||||
const originWidth = colSizeList.value[colIndex]
|
||||
const startPageX = e.pageX
|
||||
|
||||
const minWidth = 50
|
||||
@ -344,15 +373,17 @@ export default defineComponent({
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
const moveX = e.pageX - startPageX
|
||||
const moveX = (e.pageX - startPageX) / canvasScale.value
|
||||
const width = originWidth + moveX < minWidth ? minWidth : Math.round(originWidth + moveX)
|
||||
|
||||
colWidths.value[colIndex] = width
|
||||
colSizeList.value[colIndex] = width
|
||||
}
|
||||
document.onmouseup = () => {
|
||||
isMouseDown = false
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
|
||||
emit('changeColWidths', colSizeList.value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -384,14 +415,14 @@ export default defineComponent({
|
||||
|
||||
const nextCell = getNextCell(nextRow, nextCol)
|
||||
if(!nextCell) {
|
||||
insertRow(nextRow, nextRow + 1)
|
||||
insertRow(nextRow + 1)
|
||||
startCell.value = [nextRow + 1, 0]
|
||||
}
|
||||
else startCell.value = nextCell
|
||||
|
||||
nextTick(() => {
|
||||
const textRef = document.querySelector('.cell-text.active') as HTMLInputElement
|
||||
textRef.focus()
|
||||
if(textRef) textRef.focus()
|
||||
})
|
||||
}
|
||||
|
||||
@ -453,15 +484,15 @@ export default defineComponent({
|
||||
{
|
||||
text: '插入列',
|
||||
children: [
|
||||
{ text: '到左侧', handler: () => insertCol(colIndex, colIndex) },
|
||||
{ text: '到右侧', handler: () => insertCol(colIndex, colIndex + 1) },
|
||||
{ text: '到左侧', handler: () => insertCol(colIndex) },
|
||||
{ text: '到右侧', handler: () => insertCol(colIndex + 1) },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '插入行',
|
||||
children: [
|
||||
{ text: '到上方', handler: () => insertRow(rowIndex, rowIndex) },
|
||||
{ text: '到下方', handler: () => insertRow(rowIndex, rowIndex + 1) },
|
||||
{ text: '到上方', handler: () => insertRow(rowIndex) },
|
||||
{ text: '到下方', handler: () => insertRow(rowIndex + 1) },
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -506,10 +537,10 @@ export default defineComponent({
|
||||
}, 300, { trailing: true })
|
||||
|
||||
return {
|
||||
width,
|
||||
dragLinePosition,
|
||||
tableCells,
|
||||
colWidths,
|
||||
colSizeList,
|
||||
totalWidth,
|
||||
hideCells,
|
||||
selectedCells,
|
||||
activedCell,
|
||||
@ -537,16 +568,19 @@ table {
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: 0;
|
||||
word-wrap: break-word;
|
||||
user-select: none;
|
||||
|
||||
tr {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #d9d9d9;
|
||||
cursor: default;
|
||||
|
||||
&.selected::after {
|
||||
@ -561,7 +595,8 @@ table {
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
min-height: 22px;
|
||||
min-height: 32px;
|
||||
padding: 5px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
line-height: 1.5;
|
||||
@ -572,11 +607,6 @@ table {
|
||||
&.active {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgba(27, 110, 232, 0.3);
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
143
src/views/components/element/TableElement/StaticTable.vue
Normal file
143
src/views/components/element/TableElement/StaticTable.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div
|
||||
class="static-table"
|
||||
:style="{ width: totalWidth + 'px' }"
|
||||
>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col span="1" v-for="(width, index) in colSizeList" :key="index" :width="width">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(rowCells, rowIndex) in data"
|
||||
:key="rowIndex"
|
||||
>
|
||||
<td
|
||||
class="cell"
|
||||
:style="{
|
||||
borderStyle: outline.style,
|
||||
borderColor: outline.color,
|
||||
borderWidth: outline.width + 'px',
|
||||
}"
|
||||
v-for="(cell, colIndex) in rowCells"
|
||||
:key="cell.id"
|
||||
:rowspan="cell.rowspan"
|
||||
:colspan="cell.colspan"
|
||||
v-show="!hideCells.includes(`${rowIndex}_${colIndex}`)"
|
||||
>
|
||||
<div class="cell-text" v-html="cell.content" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, ref, watch } from 'vue'
|
||||
import { PPTElementOutline, TableCell } from '@/types/slides'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'static-table',
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<TableCell[][]>,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
colWidths: {
|
||||
type: Array as PropType<number[]>,
|
||||
required: true,
|
||||
},
|
||||
outline: {
|
||||
type: Object as PropType<PPTElementOutline>,
|
||||
required: true,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
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 hideCells = computed(() => {
|
||||
const hideCells = []
|
||||
|
||||
for(let i = 0; i < props.data.length; i++) {
|
||||
const rowCells = props.data[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
|
||||
})
|
||||
|
||||
return {
|
||||
colSizeList,
|
||||
totalWidth,
|
||||
hideCells,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.static-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;
|
||||
|
||||
tr {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cell-text {
|
||||
min-height: 32px;
|
||||
padding: 5px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,45 +1,61 @@
|
||||
<template>
|
||||
<div class="editable-element-shape"
|
||||
<div
|
||||
class="editable-element-table"
|
||||
ref="elementRef"
|
||||
:class="{ 'lock': elementInfo.lock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
width: elementInfo.width + 'px',
|
||||
}"
|
||||
@mousedown="$event => handleSelectElement($event)"
|
||||
>
|
||||
<div
|
||||
class="element-content"
|
||||
v-contextmenu="contextmenus"
|
||||
>
|
||||
<div
|
||||
class="table-mask"
|
||||
v-if="!editable"
|
||||
@dblclick="editable = true"
|
||||
@mousedown="$event => handleSelectElement($event)"
|
||||
></div>
|
||||
<EditableTable
|
||||
@mousedown.stop
|
||||
:data="elementInfo.data"
|
||||
:width="elementInfo.width"
|
||||
:colWidths="elementInfo.colWidths"
|
||||
:outline="elementInfo.outline"
|
||||
:editable="editable"
|
||||
@change="data => updateTableCells(data)"
|
||||
@changeColWidths="widths => updateColWidths(widths)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { PPTShapeElement, TableCell } from '@/types/slides'
|
||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { MutationTypes, State } from '@/store'
|
||||
import { PPTTableElement, TableCell } from '@/types/slides'
|
||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import EditableTable from './EditableTable.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'editable-element-shape',
|
||||
name: 'editable-element-table',
|
||||
components: {
|
||||
EditableTable,
|
||||
},
|
||||
props: {
|
||||
elementInfo: {
|
||||
type: Object as PropType<PPTShapeElement>,
|
||||
type: Object as PropType<PPTTableElement>,
|
||||
required: true,
|
||||
},
|
||||
selectElement: {
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTShapeElement, canMove?: boolean) => void>,
|
||||
type: Function as PropType<(e: MouseEvent, element: PPTTableElement, canMove?: boolean) => void>,
|
||||
required: true,
|
||||
},
|
||||
contextmenus: {
|
||||
@ -47,27 +63,107 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore<State>()
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const handleSelectElement = (e: MouseEvent) => {
|
||||
if(props.elementInfo.lock) return
|
||||
e.stopPropagation()
|
||||
|
||||
props.selectElement(e, props.elementInfo)
|
||||
}
|
||||
const editable = ref(false)
|
||||
const handleElementId = computed(() => store.state.handleElementId)
|
||||
|
||||
watch(handleElementId, () => {
|
||||
if(handleElementId.value !== props.elementInfo.id) editable.value = false
|
||||
})
|
||||
|
||||
watch(editable, () => {
|
||||
store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, editable.value)
|
||||
})
|
||||
|
||||
const elementRef = ref<HTMLElement>()
|
||||
|
||||
const isScaling = ref(false)
|
||||
const realHeightCache = ref(-1)
|
||||
|
||||
const scaleElementStateListener = (state: boolean) => {
|
||||
isScaling.value = state
|
||||
|
||||
if(state) editable.value = false
|
||||
|
||||
if(!state && realHeightCache.value !== -1) {
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
props: { height: realHeightCache.value },
|
||||
})
|
||||
realHeightCache.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
emitter.on(EmitterEvents.SCALE_ELEMENT_STATE, state => scaleElementStateListener(state))
|
||||
onUnmounted(() => {
|
||||
emitter.off(EmitterEvents.SCALE_ELEMENT_STATE, state => scaleElementStateListener(state))
|
||||
})
|
||||
|
||||
const updateTableElementHeight = (entries: ResizeObserverEntry[]) => {
|
||||
const contentRect = entries[0].contentRect
|
||||
if(!elementRef.value) return
|
||||
|
||||
const realHeight = contentRect.height
|
||||
|
||||
if(props.elementInfo.height !== realHeight) {
|
||||
if(!isScaling.value) {
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
props: { height: realHeight },
|
||||
})
|
||||
}
|
||||
else realHeightCache.value = realHeight
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateTableElementHeight)
|
||||
|
||||
onMounted(() => {
|
||||
if(elementRef.value) resizeObserver.observe(elementRef.value)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if(elementRef.value) resizeObserver.unobserve(elementRef.value)
|
||||
})
|
||||
|
||||
const updateTableCells = (data: TableCell[][]) => {
|
||||
console.log(data)
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
props: { data },
|
||||
})
|
||||
addHistorySnapshot()
|
||||
}
|
||||
const updateColWidths = (widths: number[]) => {
|
||||
const width = widths.reduce((a, b) => a + b)
|
||||
const colWidths = widths.map(item => item / width)
|
||||
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
props: { width, colWidths },
|
||||
})
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
handleSelectElement,
|
||||
updateTableCells,
|
||||
updateColWidths,
|
||||
editable,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editable-element-shape {
|
||||
.editable-element-table {
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
|
||||
@ -81,4 +177,12 @@ export default defineComponent({
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.table-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user