diff --git a/src/components/Holos/PDF/Canvas.vue b/src/components/Holos/PDF/Canvas.vue index d43e9fc..979d854 100644 --- a/src/components/Holos/PDF/Canvas.vue +++ b/src/components/Holos/PDF/Canvas.vue @@ -1,25 +1,26 @@ \ No newline at end of file diff --git a/src/components/Holos/PDF/Draggable.vue b/src/components/Holos/PDF/Draggable.vue index 4bd032c..1b07f89 100644 --- a/src/components/Holos/PDF/Draggable.vue +++ b/src/components/Holos/PDF/Draggable.vue @@ -39,24 +39,24 @@ const handleDragEnd = () => { draggable="true" @dragstart="handleDragStart" @dragend="handleDragEnd" - class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 bg-white cursor-grab hover:bg-gray-50 hover:border-blue-300 transition-colors dark:bg-primary-d dark:border-primary/20 dark:hover:bg-primary/10" + class="flex items-center gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-gray-200 bg-white cursor-grab hover:bg-gray-50 hover:border-blue-300 transition-colors" :class="{ 'opacity-50 cursor-grabbing': isDragging, 'shadow-sm hover:shadow-md': !isDragging }" > -
+
-
+
{{ title }}
-
+
diff --git a/src/components/Holos/PDF/TableEditor.vue b/src/components/Holos/PDF/TableEditor.vue new file mode 100644 index 0000000..7e5a280 --- /dev/null +++ b/src/components/Holos/PDF/TableEditor.vue @@ -0,0 +1,201 @@ + + + \ No newline at end of file diff --git a/src/components/Holos/PDF/TextFormatter.vue b/src/components/Holos/PDF/TextFormatter.vue index 66b98db..ee98acd 100644 --- a/src/components/Holos/PDF/TextFormatter.vue +++ b/src/components/Holos/PDF/TextFormatter.vue @@ -2,111 +2,153 @@ import { ref, computed, watch } from 'vue'; import GoogleIcon from '@Shared/GoogleIcon.vue'; -/** Propiedades */ const props = defineProps({ - element: { + selectedElement: { type: Object, default: null }, visible: { type: Boolean, default: false + }, + activeTextElement: { + type: Object, + default: null } }); -/** Eventos */ -const emit = defineEmits(['update']); +const emit = defineEmits(['update', 'smart-align']); -/** Propiedades computadas */ -const formatting = computed(() => props.element?.formatting || {}); -const hasTextElement = computed(() => props.element?.type === 'text'); +// Determinar si hay un elemento de texto activo (puede ser texto directo o celda de tabla) +const hasActiveText = computed(() => { + return props.visible && ( + props.selectedElement?.type === 'text' || + props.activeTextElement?.type === 'text' || + (props.selectedElement?.type === 'table' && props.activeTextElement) + ); +}); -/** Métodos */ +// Obtener formato del elemento activo +const formatting = computed(() => { + if (props.activeTextElement) { + return props.activeTextElement.formatting || {}; + } + if (props.selectedElement?.type === 'text') { + return props.selectedElement.formatting || {}; + } + return {}; +}); + +// Obtener información del elemento activo +const activeElementInfo = computed(() => { + if (props.activeTextElement) { + return { + type: 'text', + context: props.selectedElement?.type === 'table' ? 'table-cell' : 'text-element' + }; + } + if (props.selectedElement?.type === 'text') { + return { + type: 'text', + context: 'text-element' + }; + } + return null; +}); + +/** Métodos de formato */ const toggleBold = () => { - if (!hasTextElement.value) return; + if (!hasActiveText.value) return; updateFormatting('bold', !formatting.value.bold); }; const toggleItalic = () => { - if (!hasTextElement.value) return; + if (!hasActiveText.value) return; updateFormatting('italic', !formatting.value.italic); }; const toggleUnderline = () => { - if (!hasTextElement.value) return; + if (!hasActiveText.value) return; updateFormatting('underline', !formatting.value.underline); }; const updateFontSize = (size) => { - if (!hasTextElement.value) return; + if (!hasActiveText.value) return; updateFormatting('fontSize', size); }; const updateTextAlign = (align) => { - if (!hasTextElement.value) return; + if (!hasActiveText.value) return; updateFormatting('textAlign', align); }; const updateColor = (color) => { - if (!hasTextElement.value) return; + if (!hasActiveText.value) return; updateFormatting('color', color); }; const updateFormatting = (key, value) => { const newFormatting = { ...formatting.value, [key]: value }; - emit('update', { - id: props.element.id, - formatting: newFormatting - }); -}; - -const updateContainerAlign = (align) => { - if (!hasTextElement.value) return; - updateFormatting('containerAlign', align); -}; - -const updateSmartAlign = (align) => { - if (!hasTextElement.value) return; - // Emitir tanto la alineación del texto como la posición del contenedor - emit('smart-align', { - id: props.element.id, - align: align, - formatting: { - ...formatting.value, - textAlign: align, - containerAlign: align - } - }); + // Determinar qué elemento actualizar + let targetId = null; + if (props.activeTextElement) { + targetId = props.activeTextElement.id; + } else if (props.selectedElement?.type === 'text') { + targetId = props.selectedElement.id; + } + + if (targetId) { + emit('update', { + id: targetId, + formatting: newFormatting, + context: activeElementInfo.value?.context + }); + } }; -/** Colores predefinidos */ +/** Colores y tamaños predefinidos */ const predefinedColors = [ '#000000', '#333333', '#666666', '#999999', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', '#FFA500', '#800080' ]; -/** Tamaños de fuente */ const fontSizes = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72]; \ No newline at end of file diff --git a/src/pages/Maquetador/Index.vue b/src/pages/Maquetador/Index.vue index 75de218..6024e36 100644 --- a/src/pages/Maquetador/Index.vue +++ b/src/pages/Maquetador/Index.vue @@ -41,15 +41,25 @@ const moveElement = (moveData) => { /** Referencias */ const viewportRef = ref(null); -/** Referencias adicionales */ -const textFormatterElement = ref(null); +/** Referencias adicionales para TextFormatter */ +const selectedElement = ref(null); +const activeTextElement = ref(null); const showTextFormatter = ref(false); const selectElement = (elementId) => { selectedElementId.value = elementId; const selectedEl = allElements.value.find(el => el.id === elementId); - showTextFormatter.value = selectedEl?.type === 'text'; - textFormatterElement.value = selectedEl; + selectedElement.value = selectedEl; + + // Mostrar TextFormatter si es texto o tabla + showTextFormatter.value = selectedEl?.type === 'text' || selectedEl?.type === 'table'; + + // Si es texto directo, activar como activeTextElement + if (selectedEl?.type === 'text') { + activeTextElement.value = selectedEl; + } else { + activeTextElement.value = null; + } }; const updateElement = (update) => { @@ -57,40 +67,20 @@ const updateElement = (update) => { if (element) { Object.assign(element, update); - // Si se actualiza el formato, actualizar la referencia - if (update.formatting && textFormatterElement.value?.id === update.id) { - textFormatterElement.value = { ...element }; + // Actualizar referencias si es necesario + if (selectedElement.value?.id === update.id) { + selectedElement.value = { ...element }; + } + if (activeTextElement.value?.id === update.id) { + activeTextElement.value = { ...element }; } } }; -const handleSmartAlign = (alignData) => { - const element = allElements.value.find(el => el.id === alignData.id); - if (element && element.type === 'text') { - const pageWidth = currentPageSize.value.width; - const elementWidth = element.width || 200; - - let newX = element.x; - switch (alignData.align) { - case 'center': - newX = (pageWidth - elementWidth) / 2; - break; - case 'right': - newX = pageWidth - elementWidth - 50; - break; - case 'left': - default: - newX = 50; - } - - // Actualizar posición y formato - Object.assign(element, { - x: newX, - formatting: alignData.formatting - }); - - textFormatterElement.value = { ...element }; - } +// Manejar selección de texto dentro de celdas de tabla +const handleTextSelected = (data) => { + activeTextElement.value = data.element; + showTextFormatter.value = true; }; /** Tipos de elementos disponibles */ @@ -270,9 +260,6 @@ const exportPDF = async () => { // Convertir dimensiones de pixels a puntos (1 px = 0.75 puntos) const pageWidthPoints = currentPageSize.value.width * 0.75; const pageHeightPoints = currentPageSize.value.height * 0.75; - - let currentPDFPage = pdfDoc.addPage([pageWidthPoints, pageHeightPoints]); - let currentPageHeight = pageHeightPoints; // Obtener fuentes const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); @@ -283,11 +270,9 @@ const exportPDF = async () => { for (let pageIndex = 0; pageIndex < pages.value.length; pageIndex++) { const page = pages.value[pageIndex]; - // Crear nueva página PDF si no es la primera - if (pageIndex > 0) { - currentPDFPage = pdfDoc.addPage([pageWidthPoints, pageHeightPoints]); - currentPageHeight = pageHeightPoints; - } + // Crear nueva página PDF + let currentPDFPage = pdfDoc.addPage([pageWidthPoints, pageHeightPoints]); + let currentPageHeight = pageHeightPoints; // Ordenar elementos de esta página por posición const sortedElements = [...page.elements].sort((a, b) => { @@ -308,16 +293,15 @@ const exportPDF = async () => { switch (element.type) { case 'text': - if (element.content) { + if (element.content && element.content.trim()) { // Obtener formato del elemento const formatting = element.formatting || {}; - const fontSize = formatting.fontSize || 12; + const fontSize = Math.max(8, Math.min(72, formatting.fontSize || 12)); const isBold = formatting.bold || false; - const isItalic = formatting.italic || false; const textAlign = formatting.textAlign || 'left'; // Convertir color hexadecimal a RGB - let textColor = rgb(0, 0, 0); // Negro por defecto + let textColor = rgb(0, 0, 0); if (formatting.color) { const hex = formatting.color.replace('#', ''); const r = parseInt(hex.substr(0, 2), 16) / 255; @@ -327,51 +311,66 @@ const exportPDF = async () => { } // Seleccionar fuente - let font = helveticaFont; - if (isBold && !isItalic) { - font = helveticaBoldFont; - } else if (isBold && isItalic) { - // Para negrita + cursiva, usar negrita (limitación de PDF-lib) - font = helveticaBoldFont; - } + let font = isBold ? helveticaBoldFont : helveticaFont; - // Dividir texto largo en líneas - const words = element.content.split(' '); - const lines = []; - let currentLine = ''; + // NUEVO: Dividir por párrafos (saltos de línea) + const paragraphs = element.content.split('\n'); const maxWidth = Math.min(pageWidthPoints - 100, (element.width || 200) * scaleX); + const lineHeight = fontSize * 1.4; + let currentY = y; - for (const word of words) { - const testLine = currentLine + (currentLine ? ' ' : '') + word; - const testWidth = font.widthOfTextAtSize(testLine, fontSize); - - if (testWidth <= maxWidth) { - currentLine = testLine; - } else { - if (currentLine) lines.push(currentLine); - currentLine = word; - } - } - if (currentLine) lines.push(currentLine); - - // Calcular posición X según alineación - lines.forEach((line, index) => { - let lineX = x; - const lineWidth = font.widthOfTextAtSize(line, fontSize); - - if (textAlign === 'center') { - lineX = x + (maxWidth - lineWidth) / 2; - } else if (textAlign === 'right') { - lineX = x + maxWidth - lineWidth; + paragraphs.forEach((paragraph, paragraphIndex) => { + if (paragraph.trim() === '') { + // Párrafo vacío - solo agregar espacio + currentY -= lineHeight; + return; } - currentPDFPage.drawText(line, { - x: Math.max(50, lineX), - y: y - (index * (fontSize + 3)), - size: fontSize, - font: font, - color: textColor, + // Dividir párrafo en líneas que caben en el ancho + const words = paragraph.split(' '); + const lines = []; + let currentLine = ''; + + for (const word of words) { + const testLine = currentLine + (currentLine ? ' ' : '') + word; + const testWidth = font.widthOfTextAtSize(testLine, fontSize); + + if (testWidth <= maxWidth) { + currentLine = testLine; + } else { + if (currentLine) lines.push(currentLine); + currentLine = word; + } + } + if (currentLine) lines.push(currentLine); + + // Dibujar cada línea del párrafo + lines.forEach((line, lineIndex) => { + let lineX = x; + const lineWidth = font.widthOfTextAtSize(line, fontSize); + + // Calcular posición X según alineación + if (textAlign === 'center') { + lineX = x + (maxWidth - lineWidth) / 2; + } else if (textAlign === 'right') { + lineX = x + maxWidth - lineWidth; + } + + currentPDFPage.drawText(line, { + x: Math.max(50, lineX), + y: currentY, + size: fontSize, + font: font, + color: textColor, + }); + + currentY -= lineHeight; }); + + // Agregar espacio extra entre párrafos + if (paragraphIndex < paragraphs.length - 1) { + currentY -= lineHeight * 0.3; + } }); } break; @@ -380,10 +379,7 @@ const exportPDF = async () => { if (element.content && element.content.startsWith('data:image')) { try { const base64Data = element.content.split(',')[1]; - const imageBytes = Uint8Array.from( - atob(base64Data), - c => c.charCodeAt(0) - ); + const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); let image; if (element.content.includes('image/jpeg') || element.content.includes('image/jpg')) { @@ -424,7 +420,7 @@ const exportPDF = async () => { borderWidth: 1, }); - currentPDFPage.drawText('[Imagen no disponible]', { + currentPDFPage.drawText('[Error al cargar imagen]', { x: x + 5, y: y - 35, size: 10, @@ -432,74 +428,95 @@ const exportPDF = async () => { color: rgb(0.7, 0.7, 0.7), }); } - } else { - // Placeholder para imagen vacía - currentPDFPage.drawRectangle({ - x: x, - y: y - 60, - width: 100 * scaleX, - height: 60 * scaleY, - borderColor: rgb(0.7, 0.7, 0.7), - borderWidth: 1, - borderDashArray: [3, 3], - }); - - currentPDFPage.drawText('[Imagen]', { - x: x + 25, - y: y - 35, - size: 10, - font: helveticaFont, - color: rgb(0.7, 0.7, 0.7), - }); } break; case 'table': - if (element.content && element.content.data) { + if (element.content && element.content.data && Array.isArray(element.content.data)) { const tableData = element.content.data; - const cellWidth = 80 * scaleX; - const cellHeight = 20 * scaleY; - const tableWidth = tableData[0].length * cellWidth; - const tableHeight = tableData.length * cellHeight; + const totalCols = tableData[0]?.length || 0; + const totalRows = tableData.length; + + if (totalCols > 0 && totalRows > 0) { + const availableWidth = Math.min(pageWidthPoints - 100, (element.width || 300) * scaleX); + const cellWidth = availableWidth / totalCols; + const cellHeight = 25; + const tableWidth = cellWidth * totalCols; + const tableHeight = cellHeight * totalRows; - currentPDFPage.drawRectangle({ - x: x, - y: y - tableHeight, - width: tableWidth, - height: tableHeight, - borderColor: rgb(0.3, 0.3, 0.3), - borderWidth: 1, - }); - - tableData.forEach((row, rowIndex) => { - row.forEach((cell, colIndex) => { - const cellX = x + (colIndex * cellWidth); - const cellY = y - (rowIndex * cellHeight) - 15; - - currentPDFPage.drawRectangle({ - x: cellX, - y: y - ((rowIndex + 1) * cellHeight), - width: cellWidth, - height: cellHeight, - borderColor: rgb(0.7, 0.7, 0.7), - borderWidth: 0.5, - }); - - const maxChars = Math.floor(cellWidth / 8); - const displayText = cell.length > maxChars ? - cell.substring(0, maxChars - 3) + '...' : cell; - - currentPDFPage.drawText(displayText, { - x: cellX + 5, - y: cellY, - size: 8, - font: helveticaFont, - color: rowIndex === 0 ? rgb(0.2, 0.2, 0.8) : rgb(0, 0, 0), - }); + // Dibujar borde exterior de la tabla + currentPDFPage.drawRectangle({ + x: x, + y: y - tableHeight, + width: tableWidth, + height: tableHeight, + borderColor: rgb(0.2, 0.2, 0.2), + borderWidth: 1.5, }); - }); + + // Dibujar cada celda + tableData.forEach((row, rowIndex) => { + if (Array.isArray(row)) { + row.forEach((cell, colIndex) => { + const cellX = x + (colIndex * cellWidth); + const cellY = y - (rowIndex * cellHeight); + + // Dibujar borde de celda + currentPDFPage.drawRectangle({ + x: cellX, + y: cellY - cellHeight, + width: cellWidth, + height: cellHeight, + borderColor: rgb(0.7, 0.7, 0.7), + borderWidth: 0.5, + }); + + // Fondo especial para headers + if (rowIndex === 0) { + currentPDFPage.drawRectangle({ + x: cellX, + y: cellY - cellHeight, + width: cellWidth, + height: cellHeight, + color: rgb(0.95, 0.95, 1), + }); + } + + // Dibujar texto de la celda + if (cell && typeof cell === 'string' && cell.trim()) { + const maxChars = Math.max(1, Math.floor(cellWidth / 6)); + let displayText = cell.trim(); + + if (displayText.length > maxChars) { + displayText = displayText.substring(0, maxChars - 3) + '...'; + } + + const fontSize = Math.max(8, Math.min(12, cellWidth / 8)); + const font = rowIndex === 0 ? helveticaBoldFont : helveticaFont; + const textColor = rowIndex === 0 ? rgb(0.1, 0.1, 0.6) : rgb(0, 0, 0); + + const textWidth = font.widthOfTextAtSize(displayText, fontSize); + const textX = cellX + (cellWidth - textWidth) / 2; + const textY = cellY - cellHeight/2 - fontSize/2; + + currentPDFPage.drawText(displayText, { + x: Math.max(cellX + 2, textX), + y: Math.max(cellY - cellHeight + 5, textY), + size: fontSize, + font: font, + color: textColor, + }); + } + }); + } + }); + } } break; + + default: + console.warn(`Tipo de elemento no soportado: ${element.type}`); + break; } } } @@ -522,7 +539,7 @@ const exportPDF = async () => { } catch (error) { console.error('Error al generar PDF:', error); - window.Notify.error('Error al generar el PDF'); + window.Notify.error('Error al generar el PDF: ' + error.message); } finally { isExporting.value = false; } @@ -574,14 +591,14 @@ onMounted(() => {