完成基础表格元素

This commit is contained in:
pipipi-pikachu 2021-01-23 18:43:31 +08:00
parent e3f20147db
commit a1515e1994
13 changed files with 419 additions and 95 deletions

View File

@ -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) => {

View File

@ -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: '' },

View File

@ -160,6 +160,7 @@ export interface PPTTableElement {
groupId?: string;
width: number;
height: number;
outline: PPTElementOutline;
colWidths: number[];
data: TableCell[][];
}

View File

@ -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)

View File

@ -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
})

View File

@ -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
})

View File

@ -1,5 +1,6 @@
<template>
<div class="base-element-shape"
<div
class="base-element-shape"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',

View File

@ -1,5 +1,6 @@
<template>
<div class="editable-element-shape"
<div
class="editable-element-shape"
:class="{ 'lock': elementInfo.lock }"
:style="{
top: elementInfo.top + 'px',

View File

@ -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>

View File

@ -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,

View File

@ -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;
}
}
}

View 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>

View File

@ -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>