feat: 分页支持持续分页
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"@tiptap/extension-table-cell": "^3.11.0",
|
||||
"@tiptap/extension-table-header": "^3.11.0",
|
||||
"@tiptap/extension-table-row": "^3.11.0",
|
||||
"@tiptap/extension-text-align": "^3.11.0",
|
||||
"@tiptap/pm": "^3.11.0",
|
||||
"@tiptap/starter-kit": "^3.11.0",
|
||||
"@tiptap/suggestion": "^3.11.0",
|
||||
@@ -30,6 +31,7 @@
|
||||
"lucide-vue-next": "^0.554.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"shiki": "^3.15.0",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
51
pnpm-lock.yaml
generated
51
pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ importers:
|
||||
'@tiptap/extension-table-row':
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0(@tiptap/extension-table@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))
|
||||
'@tiptap/extension-text-align':
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))
|
||||
'@tiptap/pm':
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0
|
||||
@@ -74,6 +77,9 @@ importers:
|
||||
shiki:
|
||||
specifier: ^3.15.0
|
||||
version: 3.15.0
|
||||
tiptap-markdown:
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))
|
||||
vue:
|
||||
specifier: ^3.5.24
|
||||
version: 3.5.24(typescript@5.9.3)
|
||||
@@ -443,6 +449,11 @@ packages:
|
||||
'@tiptap/core': ^3.11.0
|
||||
'@tiptap/pm': ^3.11.0
|
||||
|
||||
'@tiptap/extension-text-align@3.11.0':
|
||||
resolution: {integrity: sha512-Hmcnc10vP2TecVYEuIKpx9HPWXQ263Vaqq8BoplXIt7XQ+pCZFS/TF6F8zcClb8gMIhICI89GzF4TEvxnHlxFw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^3.11.0
|
||||
|
||||
'@tiptap/extension-text@3.11.0':
|
||||
resolution: {integrity: sha512-ELAYm2BuChzZOqDG9B0k3W6zqM4pwNvXkam28KgHGiT2y7Ni68Rb+NXp16uVR+5zR6hkqnQ/BmJSKzAW59MXpA==}
|
||||
peerDependencies:
|
||||
@@ -495,15 +506,24 @@ packages:
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/linkify-it@3.0.5':
|
||||
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||
|
||||
'@types/mdurl@1.0.5':
|
||||
resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
@@ -754,6 +774,9 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
markdown-it-task-lists@2.1.1:
|
||||
resolution: {integrity: sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
@@ -957,6 +980,11 @@ packages:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tiptap-markdown@0.9.0:
|
||||
resolution: {integrity: sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^3.0.1
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
@@ -1324,6 +1352,10 @@ snapshots:
|
||||
'@tiptap/core': 3.11.0(@tiptap/pm@3.11.0)
|
||||
'@tiptap/pm': 3.11.0
|
||||
|
||||
'@tiptap/extension-text-align@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.11.0(@tiptap/pm@3.11.0)
|
||||
|
||||
'@tiptap/extension-text@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.11.0(@tiptap/pm@3.11.0)
|
||||
@@ -1418,8 +1450,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/linkify-it@3.0.5': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/markdown-it@13.0.9':
|
||||
dependencies:
|
||||
'@types/linkify-it': 3.0.5
|
||||
'@types/mdurl': 1.0.5
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
@@ -1429,6 +1468,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/mdurl@1.0.5': {}
|
||||
|
||||
'@types/mdurl@2.0.0': {}
|
||||
|
||||
'@types/node@24.10.1':
|
||||
@@ -1669,6 +1710,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
markdown-it-task-lists@2.1.1: {}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@@ -1919,6 +1962,14 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tiptap-markdown@0.9.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)):
|
||||
dependencies:
|
||||
'@tiptap/core': 3.11.0(@tiptap/pm@3.11.0)
|
||||
'@types/markdown-it': 13.0.9
|
||||
markdown-it: 14.1.0
|
||||
markdown-it-task-lists: 2.1.1
|
||||
prosemirror-markdown: 1.13.2
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
tslib@2.8.1:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="editor-container" :class="{ dark: theme === 'dark' }" v-if="editor">
|
||||
<div class="editor-header">
|
||||
<button @click="exportHTML" class="export-btn">Export HTML</button>
|
||||
<button @click="exportMarkdown" class="export-btn">Export Markdown</button>
|
||||
</div>
|
||||
<drag-handle
|
||||
class="drag-handle"
|
||||
:editor="editor"
|
||||
@@ -45,6 +49,8 @@
|
||||
</floating-menu>
|
||||
|
||||
<editor-content :editor="editor" class="editor-content" />
|
||||
|
||||
<table-controls :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -54,11 +60,13 @@ import { BubbleMenu, FloatingMenu } from '@tiptap/vue-3/menus'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
import { Table } from '@tiptap/extension-table'
|
||||
import { TableCell } from '@tiptap/extension-table-cell'
|
||||
import { TableHeader } from '@tiptap/extension-table-header'
|
||||
import { TableRow } from '@tiptap/extension-table-row'
|
||||
import { DragHandle } from '@tiptap/extension-drag-handle-vue-3'
|
||||
import { Markdown } from 'tiptap-markdown'
|
||||
import SlashCommand from '../extensions/SlashCommand'
|
||||
import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom'
|
||||
import {
|
||||
@@ -78,6 +86,7 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import SlashCommandList from './SlashCommandList.vue'
|
||||
import CodeBlockComponent from './CodeBlockComponent.vue'
|
||||
import TableControls from './TableControls.vue'
|
||||
import { all, createLowlight } from 'lowlight'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -262,11 +271,64 @@ const editor = useEditor({
|
||||
render: renderSuggestion,
|
||||
},
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph', 'tableCell', 'tableHeader'],
|
||||
}),
|
||||
Markdown,
|
||||
],
|
||||
content: '<p>Hello World!</p>',
|
||||
})
|
||||
|
||||
const exportHTML = () => {
|
||||
const html = editor.value?.getHTML()
|
||||
if (html) {
|
||||
const blob = new Blob([html], { type: 'text/html' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'document.html'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
const exportMarkdown = () => {
|
||||
const markdown = (editor.value?.storage as any).markdown.getMarkdown()
|
||||
if (markdown) {
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'document.md'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import '../styles/index.css';
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="slash-command-list">
|
||||
<div class="slash-command-list" ref="containerRef">
|
||||
<template v-if="items.length">
|
||||
<button
|
||||
v-for="(item, index) in items"
|
||||
@@ -8,6 +8,7 @@
|
||||
:class="{ 'is-selected': index === selectedIndex }"
|
||||
@click="selectItem(index)"
|
||||
@mouseenter="selectedIndex = index"
|
||||
ref="itemRefs"
|
||||
>
|
||||
<component :is="item.icon" class="icon" />
|
||||
<div class="label">
|
||||
@@ -23,7 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
interface CommandItem {
|
||||
@@ -39,6 +40,8 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const selectedIndex = ref(0)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const itemRefs = ref<HTMLElement[]>([])
|
||||
|
||||
watch(
|
||||
() => props.items,
|
||||
@@ -47,6 +50,28 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(selectedIndex, () => {
|
||||
scrollToSelected()
|
||||
})
|
||||
|
||||
const scrollToSelected = () => {
|
||||
nextTick(() => {
|
||||
const container = containerRef.value
|
||||
const item = itemRefs.value[selectedIndex.value]
|
||||
|
||||
if (container && item) {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const itemRect = item.getBoundingClientRect()
|
||||
|
||||
if (itemRect.bottom > containerRect.bottom) {
|
||||
container.scrollTop += itemRect.bottom - containerRect.bottom
|
||||
} else if (itemRect.top < containerRect.top) {
|
||||
container.scrollTop -= containerRect.top - itemRect.top
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onKeyDown = ({ event }: { event: KeyboardEvent }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler()
|
||||
|
||||
342
src/components/TableControls.vue
Normal file
342
src/components/TableControls.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div class="table-controls" v-if="editor">
|
||||
<!-- Row Handle -->
|
||||
<div
|
||||
v-if="activeCell"
|
||||
class="table-handle row-handle"
|
||||
:style="rowHandleStyle"
|
||||
@click.stop="showRowMenu"
|
||||
>
|
||||
<div class="handle-icon">⋮</div>
|
||||
</div>
|
||||
|
||||
<!-- Column Handle -->
|
||||
<div
|
||||
v-if="activeCell"
|
||||
class="table-handle col-handle"
|
||||
:style="colHandleStyle"
|
||||
@click.stop="showColMenu"
|
||||
>
|
||||
<div class="handle-icon">···</div>
|
||||
</div>
|
||||
|
||||
<!-- Row Menu -->
|
||||
<div v-if="activeRowMenu" class="table-menu" :style="rowMenuStyle">
|
||||
<button @click="addRowAbove">Add Row Above</button>
|
||||
<button @click="addRowBelow">Add Row Below</button>
|
||||
<button @click="deleteRow" class="danger">Delete Row</button>
|
||||
</div>
|
||||
|
||||
<!-- Column Menu -->
|
||||
<div v-if="activeColMenu" class="table-menu" :style="colMenuStyle">
|
||||
<button @click="addColumnLeft">Add Column Left</button>
|
||||
<button @click="addColumnRight">Add Column Right</button>
|
||||
<div class="alignment-controls">
|
||||
<button
|
||||
@click="alignLeft"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
|
||||
title="Align Left"
|
||||
>
|
||||
<AlignLeft :size="16" />
|
||||
</button>
|
||||
<button
|
||||
@click="alignCenter"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
|
||||
title="Align Center"
|
||||
>
|
||||
<AlignCenter :size="16" />
|
||||
</button>
|
||||
<button
|
||||
@click="alignRight"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
|
||||
title="Align Right"
|
||||
>
|
||||
<AlignRight :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<button @click="deleteColumn" class="danger">Delete Column</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { Editor } from '@tiptap/vue-3'
|
||||
import { AlignLeft, AlignCenter, AlignRight } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor
|
||||
}>()
|
||||
|
||||
const activeCell = ref<HTMLElement | null>(null)
|
||||
const activeTable = ref<HTMLElement | null>(null)
|
||||
const activeRowMenu = ref(false)
|
||||
const activeColMenu = ref(false)
|
||||
const menuPosition = ref({ x: 0, y: 0 })
|
||||
const hideTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const rowHandleStyle = computed(() => {
|
||||
if (!activeCell.value || !activeTable.value) return {}
|
||||
const cellRect = activeCell.value.getBoundingClientRect()
|
||||
const tableRect = activeTable.value.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
top: `${cellRect.top}px`,
|
||||
left: `${tableRect.left - 24}px`,
|
||||
height: `${cellRect.height}px`,
|
||||
}
|
||||
})
|
||||
|
||||
const colHandleStyle = computed(() => {
|
||||
if (!activeCell.value || !activeTable.value) return {}
|
||||
const cellRect = activeCell.value.getBoundingClientRect()
|
||||
const tableRect = activeTable.value.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
top: `${tableRect.top - 24}px`,
|
||||
left: `${cellRect.left}px`,
|
||||
width: `${cellRect.width}px`,
|
||||
}
|
||||
})
|
||||
|
||||
const rowMenuStyle = computed(() => ({
|
||||
top: `${menuPosition.value.y}px`,
|
||||
left: `${menuPosition.value.x}px`,
|
||||
}))
|
||||
|
||||
const colMenuStyle = computed(() => ({
|
||||
top: `${menuPosition.value.y}px`,
|
||||
left: `${menuPosition.value.x}px`,
|
||||
}))
|
||||
|
||||
const updateActiveCell = (event: Event) => {
|
||||
if (activeRowMenu.value || activeColMenu.value) return
|
||||
|
||||
const target = event.target as HTMLElement
|
||||
const cell = target.closest('td, th') as HTMLElement
|
||||
const isControl = target.closest('.table-handle') || target.closest('.table-menu')
|
||||
|
||||
if (cell || isControl) {
|
||||
if (hideTimeout.value) {
|
||||
clearTimeout(hideTimeout.value)
|
||||
hideTimeout.value = null
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
activeCell.value = cell
|
||||
activeTable.value = cell.closest('table') as HTMLElement
|
||||
}
|
||||
} else {
|
||||
if (!hideTimeout.value) {
|
||||
hideTimeout.value = setTimeout(() => {
|
||||
activeCell.value = null
|
||||
activeTable.value = null
|
||||
hideTimeout.value = null
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showRowMenu = (event: MouseEvent) => {
|
||||
activeRowMenu.value = true
|
||||
activeColMenu.value = false
|
||||
const rect = (event.target as HTMLElement).getBoundingClientRect()
|
||||
menuPosition.value = {
|
||||
x: rect.left,
|
||||
y: rect.bottom + 5
|
||||
}
|
||||
|
||||
// Select the cell to ensure commands apply to the correct row
|
||||
selectHoveredCell()
|
||||
}
|
||||
|
||||
const showColMenu = (event: MouseEvent) => {
|
||||
activeColMenu.value = true
|
||||
activeRowMenu.value = false
|
||||
const rect = (event.target as HTMLElement).getBoundingClientRect()
|
||||
menuPosition.value = {
|
||||
x: rect.left,
|
||||
y: rect.bottom + 5
|
||||
}
|
||||
|
||||
// Select the cell to ensure commands apply to the correct column
|
||||
selectHoveredCell()
|
||||
}
|
||||
|
||||
const selectHoveredCell = () => {
|
||||
if (!activeCell.value) return
|
||||
|
||||
const pos = props.editor.view.posAtDOM(activeCell.value, 0)
|
||||
props.editor.chain().setTextSelection(pos).run()
|
||||
}
|
||||
|
||||
const closeMenus = (event: MouseEvent) => {
|
||||
if (!(event.target as HTMLElement).closest('.table-menu') &&
|
||||
!(event.target as HTMLElement).closest('.table-handle')) {
|
||||
activeRowMenu.value = false
|
||||
activeColMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Operations
|
||||
const addRowAbove = () => {
|
||||
props.editor.chain().focus().addRowBefore().run()
|
||||
activeRowMenu.value = false
|
||||
}
|
||||
|
||||
const addRowBelow = () => {
|
||||
props.editor.chain().focus().addRowAfter().run()
|
||||
activeRowMenu.value = false
|
||||
}
|
||||
|
||||
const deleteRow = () => {
|
||||
props.editor.chain().focus().deleteRow().run()
|
||||
activeRowMenu.value = false
|
||||
activeCell.value = null
|
||||
}
|
||||
|
||||
const addColumnLeft = () => {
|
||||
props.editor.chain().focus().addColumnBefore().run()
|
||||
activeColMenu.value = false
|
||||
}
|
||||
|
||||
const addColumnRight = () => {
|
||||
props.editor.chain().focus().addColumnAfter().run()
|
||||
activeColMenu.value = false
|
||||
}
|
||||
|
||||
const deleteColumn = () => {
|
||||
props.editor.chain().focus().deleteColumn().run()
|
||||
activeColMenu.value = false
|
||||
activeCell.value = null
|
||||
}
|
||||
|
||||
const alignLeft = () => props.editor.chain().focus().setTextAlign('left').run()
|
||||
const alignCenter = () => props.editor.chain().focus().setTextAlign('center').run()
|
||||
const alignRight = () => props.editor.chain().focus().setTextAlign('right').run()
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', updateActiveCell)
|
||||
document.addEventListener('click', closeMenus)
|
||||
window.addEventListener('scroll', updateActiveCell, { capture: true })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', updateActiveCell)
|
||||
document.removeEventListener('click', closeMenus)
|
||||
window.removeEventListener('scroll', updateActiveCell, { capture: true })
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table-controls {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.table-handle {
|
||||
position: absolute;
|
||||
background-color: var(--bg-secondary, #f1f5f9);
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
transition: background-color 0.2s;
|
||||
color: var(--text-secondary, #64748b);
|
||||
}
|
||||
|
||||
.table-handle:hover {
|
||||
background-color: var(--bg-hover, #e2e8f0);
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.row-handle {
|
||||
width: 20px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.col-handle {
|
||||
height: 20px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.handle-icon {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.table-menu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
pointer-events: auto;
|
||||
z-index: 20;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.table-menu button {
|
||||
text-align: left;
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.table-menu button:hover {
|
||||
background-color: var(--bg-hover, #f1f5f9);
|
||||
}
|
||||
|
||||
.table-menu button.danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.table-menu button.danger:hover {
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
.alignment-controls {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid var(--border-color, #e2e8f0);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alignment-controls button {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #64748b);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.alignment-controls button:hover {
|
||||
background-color: var(--bg-hover, #f1f5f9);
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
|
||||
.alignment-controls button.is-active {
|
||||
background-color: var(--bg-active, #e2e8f0);
|
||||
color: var(--text-primary, #1e293b);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user