Cambios minimos al canvas

This commit is contained in:
Juan Felipe Zapata Moreno 2025-09-26 16:14:42 -06:00
parent 21f5d3a761
commit b2095e4559
4 changed files with 133 additions and 51 deletions

View File

@ -6,6 +6,7 @@ import TiptapEditor from "./TiptapEditor.vue";
const props = defineProps({ const props = defineProps({
element: { type: Object, required: true }, element: { type: Object, required: true },
isSelected: { type: Boolean, default: false }, isSelected: { type: Boolean, default: false },
pageDimensions: { type: Object, required: true },
}); });
const emit = defineEmits([ const emit = defineEmits([
"select", "select",
@ -100,13 +101,19 @@ const handleMouseMove = (event) => {
if (isDragging.value) { if (isDragging.value) {
const deltaX = event.clientX - dragStart.value.mouseX; const deltaX = event.clientX - dragStart.value.mouseX;
const deltaY = event.clientY - dragStart.value.mouseY; const deltaY = event.clientY - dragStart.value.mouseY;
const newX = dragStart.value.elementX + deltaX; let newX = dragStart.value.elementX + deltaX;
const newY = dragStart.value.elementY + deltaY; let newY = dragStart.value.elementY + deltaY;
emit("move", {
id: props.element.id, // Límites
x: Math.max(0, newX), const pageW = props.pageDimensions.width;
y: Math.max(0, newY), const pageH = props.pageDimensions.height;
}); const elW = props.element.width;
const elH = props.element.height;
newX = Math.max(0, Math.min(newX, pageW - elW));
newY = Math.max(0, Math.min(newY, pageH - elH));
emit("move", { id: props.element.id, x: newX, y: newY });
} else if (isResizing.value) { } else if (isResizing.value) {
handleResizeMove(event); handleResizeMove(event);
} }
@ -138,8 +145,19 @@ const handleResizeMove = (event) => {
if (!isResizing.value) return; if (!isResizing.value) return;
const deltaX = event.clientX - resizeStart.value.x; const deltaX = event.clientX - resizeStart.value.x;
const deltaY = event.clientY - resizeStart.value.y; const deltaY = event.clientY - resizeStart.value.y;
const newWidth = Math.max(100, resizeStart.value.width + deltaX);
const newHeight = Math.max(40, resizeStart.value.height + deltaY); // Límites
const pageW = props.pageDimensions.width;
const pageH = props.pageDimensions.height;
const elX = props.element.x;
const elY = props.element.y;
let newWidth = resizeStart.value.width + deltaX;
let newHeight = resizeStart.value.height + deltaY;
newWidth = Math.max(100, Math.min(newWidth, pageW - elX));
newHeight = Math.max(40, Math.min(newHeight, pageH - elY));
emit("update", { id: props.element.id, width: newWidth, height: newHeight }); emit("update", { id: props.element.id, width: newWidth, height: newHeight });
}; };
@ -159,6 +177,17 @@ const handleFileSelect = (event) => {
} }
event.target.value = null; event.target.value = null;
}; };
const textStyles = computed(() => {
if (props.element.type !== "text") return {};
return {
width: "100%",
height: "100%",
overflow: "hidden",
position: "relative",
};
});
</script> </script>
<template> <template>
@ -181,13 +210,19 @@ const handleFileSelect = (event) => {
class="absolute inset-0 z-10 cursor-move" class="absolute inset-0 z-10 cursor-move"
/> />
<TiptapEditor <div
v-if="element.type === 'text'" v-if="element.type === 'text'"
:model-value="element.content" :style="textStyles"
:editable="isEditing" class="text-container"
@update:model-value="handleContentUpdate" >
@focus="handleEditorFocus" <TiptapEditor
/> class="w-full h-full overflow-auto"
:model-value="element.content"
:editable="isEditing"
@update:model-value="handleContentUpdate"
@focus="handleEditorFocus"
/>
</div>
<div <div
v-else-if="element.type === 'image'" v-else-if="element.type === 'image'"

View File

@ -17,13 +17,11 @@ const pageSize = ref('A4');
const pageSizes = { const pageSizes = {
'A4': { width: 794, height: 1123, label: 'A4 (210 x 297 mm)' }, 'A4': { width: 794, height: 1123, label: 'A4 (210 x 297 mm)' },
'Letter': { width: 816, height: 1056, label: 'Carta (216 x 279 mm)' },
'A3': { width: 1123, height: 1587, label: 'A3 (297 x 420 mm)' }, 'A3': { width: 1123, height: 1587, label: 'A3 (297 x 420 mm)' },
'Legal': { width: 816, height: 1344, label: 'Oficio (216 x 356 mm)' },
'Tabloid': { width: 1056, height: 1632, label: 'Tabloide (279 x 432 mm)' } 'Tabloid': { width: 1056, height: 1632, label: 'Tabloide (279 x 432 mm)' }
}; };
const ZOOM_LEVEL = 0.65; const ZOOM_LEVEL = 1.0;
/** Propiedades computadas */ /** Propiedades computadas */
const currentPageSize = computed(() => pageSizes[pageSize.value] || pageSizes['A4']); const currentPageSize = computed(() => pageSizes[pageSize.value] || pageSizes['A4']);
@ -134,7 +132,7 @@ const deletePage = (pageIndex) => {
class="absolute inset-0" class="absolute inset-0"
:style="{ transform: `scale(${ZOOM_LEVEL})`, transformOrigin: 'top left' }" :style="{ transform: `scale(${ZOOM_LEVEL})`, transformOrigin: 'top left' }"
> >
<slot name="elements" :page="page" /> <slot name="elements" :page="page" :dimensions="currentPageSize" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -52,37 +52,70 @@ defineExpose({ editor });
</script> </script>
<template> <template>
<EditorContent :editor="editor" /> <div class="tiptap-wrapper">
<EditorContent :editor="editor" />
</div>
</template> </template>
<style scoped> <style scoped>
.tiptap-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
:deep(.ProseMirror) { :deep(.ProseMirror) {
padding: 0.5rem; padding: 0.5rem;
outline: none; outline: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
line-height: 1.4;
} }
/* Prevenir que el texto se desborde horizontalmente */
:deep(.ProseMirror p) {
margin: 0 0 0.5em 0;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
max-width: 100%;
}
/* Asegurar que spans respeten el ancho */
:deep(.ProseMirror span) {
display: inline;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
}
/* Estilos para cuando NO es editable */ /* Estilos para cuando NO es editable */
:deep(.ProseMirror[contenteditable="false"]) { :deep(.ProseMirror[contenteditable="false"]) {
cursor: default; cursor: default;
overflow: hidden;
} }
/* Estilos para cuando SÍ es editable */ /* Estilos para cuando SÍ es editable */
:deep(.ProseMirror[contenteditable="true"]:focus) { :deep(.ProseMirror[contenteditable="true"]:focus) {
outline: none; outline: none;
box-shadow: none; box-shadow: none;
} }
/* Reglas generales que ya teníamos */
:deep(.ProseMirror span[style]) { /* Controlar listas */
display: inline !important;
}
:deep(.ProseMirror p) {
margin: 0;
}
:deep(.ProseMirror ul), :deep(.ProseMirror ul),
:deep(.ProseMirror ol) { :deep(.ProseMirror ol) {
padding-left: 1.5rem; padding-left: 1.5rem;
margin: 0 0 0.5em 0;
} }
/* Alineación de texto */
:deep(p[style*="text-align: center"]) { :deep(p[style*="text-align: center"]) {
text-align: center; text-align: center;
} }
@ -92,4 +125,4 @@ defineExpose({ editor });
:deep(p[style*="text-align: left"]) { :deep(p[style*="text-align: left"]) {
text-align: left; text-align: left;
} }
</style> </style>

View File

@ -126,30 +126,46 @@ const handleKeydown = (event) => {
onMounted(() => document.addEventListener("keydown", handleKeydown)); onMounted(() => document.addEventListener("keydown", handleKeydown));
onBeforeUnmount(() => document.removeEventListener("keydown", handleKeydown)); onBeforeUnmount(() => document.removeEventListener("keydown", handleKeydown));
const PDF_CANVAS_SIZES = {
A4: { width: 794, height: 1123 },
A3: { width: 1123, height: 1587 },
Tabloid: { width: 1056, height: 1632 },
};
/* Tamaños en puntos */
const PDF_POINTS = {
A4: { width: 580.00, height: 841.89 },
A3: { width: 841.89, height: 1190.55 },
Tabloid: { width: 792, height: 1224 },
};
const exportPDF = () => { const exportPDF = () => {
isExporting.value = true; isExporting.value = true;
try { try {
const content = []; const content = [];
// calcular scale entre canvas-units y pdf-points
const canvasSize = PDF_CANVAS_SIZES[currentPageSize.value] || PDF_CANVAS_SIZES.A4;
const pdfSize = PDF_POINTS[currentPageSize.value] || PDF_POINTS.A4;
const scale = pdfSize.width / canvasSize.width;
pages.value.forEach((page, pageIndex) => { pages.value.forEach((page, pageIndex) => {
const pageContent = page.elements const pageContent = page.elements
.map((element) => { .map((element) => {
const position = { x: element.x * 0.75, y: element.y * 0.75 }; // convertir posición y dimensiones usando 'scale'
const position = { x: element.x * scale, y: element.y * scale };
if (element.type === "text" && element.content) { if (element.type === "text" && element.content) {
return { return {
columns: [ stack: htmlToPdfMake(element.content),
{ width: (element.width || 250) * scale,
stack: htmlToPdfMake(element.content),
width: (element.width || 250) * 0.75,
},
],
absolutePosition: position, absolutePosition: position,
}; };
} }
if (element.type === "image" && element.content) { if (element.type === "image" && element.content) {
return { return {
image: element.content, image: element.content,
width: (element.width || 150) * 0.75, width: (element.width || 150) * scale,
absolutePosition: position, absolutePosition: position,
}; };
} }
@ -163,19 +179,15 @@ const exportPDF = () => {
}) })
); );
const numCols = element.content.data[0]?.length || 1; const numCols = element.content.data[0]?.length || 1;
const colWidth = (element.width * 0.75) / numCols; const colWidth = ((element.width || 400) * scale) / numCols;
const widths = Array(numCols).fill(colWidth); const widths = Array(numCols).fill(colWidth);
return { return {
columns: [ table: {
{ body: body,
table: { widths: widths,
body: body, },
widths: widths, width: (element.width || 400) * scale,
},
width: element.width * 0.75,
},
],
absolutePosition: position, absolutePosition: position,
layout: { layout: {
hLineWidth: () => 0.5, hLineWidth: () => 0.5,
@ -200,12 +212,15 @@ const exportPDF = () => {
const docDefinition = { const docDefinition = {
content, content,
pageSize: currentPageSize.value.toUpperCase(), pageSize: currentPageSize.value.toUpperCase(),
pageMargins: [0, 0, 0, 0], pageMargins: [40, 40, 40, 40],
styles: {
p: {
margin: [0, 0, 0, 0],
},
},
}; };
window.pdfMake window.pdfMake.createPdf(docDefinition).download(`${documentTitle.value || "documento"}.pdf`);
.createPdf(docDefinition)
.download(`${documentTitle.value || "documento"}.pdf`);
} catch (e) { } catch (e) {
console.error("Error al exportar PDF:", e); console.error("Error al exportar PDF:", e);
alert("Hubo un error al generar el PDF."); alert("Hubo un error al generar el PDF.");
@ -305,7 +320,7 @@ const availableElements = [
@click="deselectAll" @click="deselectAll"
@page-size-change="handlePageSizeChange" @page-size-change="handlePageSizeChange"
> >
<template #elements="{ page }"> <template #elements="{ page, dimensions }">
<CanvasElement <CanvasElement
v-for="element in page.elements" v-for="element in page.elements"
:key="element.id" :key="element.id"
@ -316,6 +331,7 @@ const availableElements = [
" "
:element="element" :element="element"
:is-selected="selectedElementId === element.id" :is-selected="selectedElementId === element.id"
:page-dimensions="dimensions"
@select="selectElement" @select="selectElement"
@delete="deleteElement" @delete="deleteElement"
@update="updateElement" @update="updateElement"