355 lines
7.9 KiB
Vue
355 lines
7.9 KiB
Vue
<script setup>
|
|
import { useEditor, EditorContent } from "@tiptap/vue-3";
|
|
import { StarterKit } from "@tiptap/starter-kit";
|
|
import { Table } from "@tiptap/extension-table";
|
|
import { TableRow } from "@tiptap/extension-table-row";
|
|
import { TableCell } from "@tiptap/extension-table-cell";
|
|
import { TableHeader } from "@tiptap/extension-table-header";
|
|
import { Underline } from "@tiptap/extension-underline";
|
|
import { TextStyle } from "@tiptap/extension-text-style";
|
|
import { Color } from "@tiptap/extension-color";
|
|
import { FontSize } from "tiptap-extension-font-size";
|
|
import { TextAlign } from "@tiptap/extension-text-align";
|
|
import { watch } from "vue";
|
|
import GoogleIcon from "@Shared/GoogleIcon.vue";
|
|
|
|
const props = defineProps({
|
|
modelValue: { type: String, default: "" },
|
|
editable: { type: Boolean, default: true },
|
|
});
|
|
const emit = defineEmits(["update:modelValue", "focus", "blur"]);
|
|
|
|
const editor = useEditor({
|
|
content: props.modelValue,
|
|
editable: props.editable,
|
|
extensions: [
|
|
StarterKit,
|
|
Underline,
|
|
TextStyle,
|
|
Color.configure({ types: ["textStyle"] }),
|
|
FontSize.configure({ types: ["textStyle"] }),
|
|
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
Table.configure({
|
|
resizable: true,
|
|
HTMLAttributes: {
|
|
class: 'tiptap-table',
|
|
},
|
|
}),
|
|
TableRow,
|
|
TableHeader,
|
|
TableCell,
|
|
],
|
|
onUpdate: ({ editor }) => emit("update:modelValue", editor.getHTML()),
|
|
onFocus: ({ editor }) => emit("focus", editor),
|
|
onBlur: ({ editor }) => emit("blur", editor),
|
|
});
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(newValue) => {
|
|
if (editor.value && editor.value.getHTML() !== newValue) {
|
|
editor.value.commands.setContent(newValue, false);
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => props.editable,
|
|
(isEditable) => {
|
|
editor.value?.setEditable(isEditable);
|
|
}
|
|
);
|
|
|
|
// Acciones de tabla
|
|
const insertTable = () => {
|
|
editor.value?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
};
|
|
|
|
const addRowBefore = () => {
|
|
editor.value?.chain().focus().addRowBefore().run();
|
|
};
|
|
|
|
const addRowAfter = () => {
|
|
editor.value?.chain().focus().addRowAfter().run();
|
|
};
|
|
|
|
const deleteRow = () => {
|
|
editor.value?.chain().focus().deleteRow().run();
|
|
};
|
|
|
|
const addColumnBefore = () => {
|
|
editor.value?.chain().focus().addColumnBefore().run();
|
|
};
|
|
|
|
const addColumnAfter = () => {
|
|
editor.value?.chain().focus().addColumnAfter().run();
|
|
};
|
|
|
|
const deleteColumn = () => {
|
|
editor.value?.chain().focus().deleteColumn().run();
|
|
};
|
|
|
|
const deleteTable = () => {
|
|
editor.value?.chain().focus().deleteTable().run();
|
|
};
|
|
|
|
const mergeCells = () => {
|
|
editor.value?.chain().focus().mergeCells().run();
|
|
};
|
|
|
|
const splitCell = () => {
|
|
editor.value?.chain().focus().splitCell().run();
|
|
};
|
|
|
|
const toggleHeaderRow = () => {
|
|
editor.value?.chain().focus().toggleHeaderRow().run();
|
|
};
|
|
|
|
const toggleHeaderColumn = () => {
|
|
editor.value?.chain().focus().toggleHeaderColumn().run();
|
|
};
|
|
|
|
defineExpose({ editor });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="table-editor-wrapper">
|
|
<!-- Toolbar de tabla -->
|
|
<div v-if="editable && editor" class="table-toolbar">
|
|
<div class="toolbar-section">
|
|
<button
|
|
@click="insertTable"
|
|
class="toolbar-btn"
|
|
title="Insertar tabla"
|
|
>
|
|
<GoogleIcon name="table_chart" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="toolbar-divider"></div>
|
|
|
|
<div class="toolbar-section">
|
|
<button
|
|
@click="addRowBefore"
|
|
class="toolbar-btn"
|
|
title="Insertar fila arriba"
|
|
>
|
|
<GoogleIcon name="table_rows" />
|
|
<span class="btn-label">↑</span>
|
|
</button>
|
|
<button
|
|
@click="addRowAfter"
|
|
class="toolbar-btn"
|
|
title="Insertar fila abajo"
|
|
>
|
|
<GoogleIcon name="table_rows" />
|
|
<span class="btn-label">↓</span>
|
|
</button>
|
|
<button
|
|
@click="deleteRow"
|
|
class="toolbar-btn toolbar-btn-danger"
|
|
title="Eliminar fila"
|
|
>
|
|
<GoogleIcon name="delete" />
|
|
<span class="btn-label">Fila</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="toolbar-divider"></div>
|
|
|
|
<div class="toolbar-section">
|
|
<button
|
|
@click="addColumnBefore"
|
|
class="toolbar-btn"
|
|
title="Insertar columna a la izquierda"
|
|
>
|
|
<GoogleIcon name="view_column" />
|
|
<span class="btn-label">←</span>
|
|
</button>
|
|
<button
|
|
@click="addColumnAfter"
|
|
class="toolbar-btn"
|
|
title="Insertar columna a la derecha"
|
|
>
|
|
<GoogleIcon name="view_column" />
|
|
<span class="btn-label">→</span>
|
|
</button>
|
|
<button
|
|
@click="deleteColumn"
|
|
class="toolbar-btn toolbar-btn-danger"
|
|
title="Eliminar columna"
|
|
>
|
|
<GoogleIcon name="delete" />
|
|
<span class="btn-label">Col</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="toolbar-divider"></div>
|
|
|
|
<div class="toolbar-section">
|
|
<button
|
|
@click="mergeCells"
|
|
class="toolbar-btn"
|
|
title="Combinar celdas"
|
|
>
|
|
<GoogleIcon name="call_merge" />
|
|
</button>
|
|
<button
|
|
@click="splitCell"
|
|
class="toolbar-btn"
|
|
title="Dividir celda"
|
|
>
|
|
<GoogleIcon name="call_split" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="toolbar-divider"></div>
|
|
|
|
<div class="toolbar-section">
|
|
<button
|
|
@click="toggleHeaderRow"
|
|
class="toolbar-btn"
|
|
:class="{ 'is-active': editor.isActive('tableHeader') }"
|
|
title="Toggle fila de encabezado"
|
|
>
|
|
<GoogleIcon name="text_fields" />
|
|
<span class="btn-label">H</span>
|
|
</button>
|
|
<button
|
|
@click="deleteTable"
|
|
class="toolbar-btn toolbar-btn-danger"
|
|
title="Eliminar tabla"
|
|
>
|
|
<GoogleIcon name="delete_forever" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor -->
|
|
<EditorContent :editor="editor" class="table-editor-content" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.table-editor-wrapper {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.table-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem;
|
|
background: #f9fafb;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.toolbar-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.toolbar-divider {
|
|
width: 1px;
|
|
height: 1.5rem;
|
|
background: #d1d5db;
|
|
}
|
|
|
|
.toolbar-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.375rem 0.5rem;
|
|
background: white;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.75rem;
|
|
color: #374151;
|
|
}
|
|
|
|
.toolbar-btn:hover {
|
|
background: #f3f4f6;
|
|
border-color: #9ca3af;
|
|
}
|
|
|
|
.toolbar-btn.is-active {
|
|
background: #3b82f6;
|
|
color: white;
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.toolbar-btn-danger:hover {
|
|
background: #fee2e2;
|
|
border-color: #ef4444;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.btn-label {
|
|
font-size: 0.625rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.table-editor-content {
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
:deep(.ProseMirror) {
|
|
outline: none;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Estilos para tablas Tiptap */
|
|
:deep(.tiptap-table) {
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
width: 100%;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.tiptap-table td),
|
|
:deep(.tiptap-table th) {
|
|
min-width: 1em;
|
|
border: 1px solid #d1d5db;
|
|
padding: 0.5rem;
|
|
vertical-align: top;
|
|
box-sizing: border-box;
|
|
position: relative;
|
|
}
|
|
|
|
:deep(.tiptap-table th) {
|
|
font-weight: bold;
|
|
text-align: left;
|
|
background-color: #f9fafb;
|
|
}
|
|
|
|
:deep(.tiptap-table .selectedCell) {
|
|
background-color: #dbeafe;
|
|
}
|
|
|
|
:deep(.tiptap-table .column-resize-handle) {
|
|
position: absolute;
|
|
right: -2px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 4px;
|
|
background-color: #3b82f6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
:deep(.ProseMirror p) {
|
|
margin: 0;
|
|
}
|
|
</style>
|