Juan Felipe Zapata Moreno 1ce1fb30fc FIX: Tabla WIP
2025-09-30 16:22:56 -06:00

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>