Comunicación mediante IFrame
API de comunicación mediante Iframe
Este documento describe el protocolo de comunicación basado en postMessage para embeber el Seatmap Editor como un iframe en aplicaciones padre.
Resumen
El Seatmap Editor admite comunicación bidireccional a través de la API postMessage del navegador cuando se embebe como un iframe. Esto permite a las aplicaciones padre:
- Solicitar al editor que guarde el esquema actual
- Obtener la exportación SVG del recinto actual
- Recibir notificaciones sobre cambios del esquema y el estado de guardado
- Recibir notificaciones cuando se publica un esquema
- Monitorear el progreso de guardado de los distintos componentes (metadatos del esquema, gráficos del recinto, asignaciones de precios)
Configuración
Embeber el Editor
<iframe
id="seatmap-editor"
src="https://editor.seatmap.pro/app?token=YOUR_JWT_TOKEN"
width="100%"
height="800px"
>
</iframe>
Listener de eventos de mensaje
Configura un listener de mensajes en la ventana padre para recibir mensajes del editor:
window.addEventListener('message', (event) => {
// Verify origin if needed (recommended for production)
// if (event.origin !== 'https://editor.seatmap.pro') return;
const data = event.data;
switch (data.type) {
case 'SCHEMA_CHANGED':
console.log('Schema has changes:', data.hasChanges);
break;
case 'SCHEMA_UPDATE_SUCCESS':
console.log('Schema saved successfully');
break;
case 'SCHEMA_UPDATE_ERROR':
console.error('Schema save failed');
break;
case 'SEATMAP_UPDATE_SUCCESS':
console.log('Venue graphics saved successfully');
break;
case 'SEATMAP_UPDATE_ERROR':
console.error('Venue graphics save failed');
break;
case 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS':
console.log('Price assignments saved successfully');
break;
case 'UPDATE_PRICE_ASSIGNMENTS_ERROR':
console.error('Price assignments save failed');
break;
case 'PUBLISH_REQUEST':
console.log('Schema is being published');
break;
case 'SVG_RESPONSE':
handleSvgResponse(data);
break;
default:
console.log('Unknown message type:', data.type);
}
});
Referencia de tipos de mensaje
Resumen
Parent → Editor (entrantes):
| Message Type | Descripción | Legacy Format |
|---|---|---|
SAVE_REQUEST |
Solicita al editor guardar el esquema actual | 'SAVE' |
FETCH_SVG |
Solicita la exportación SVG del esquema actual | - |
SET_LANGUAGE |
Cambia el idioma de la interfaz del editor | - |
Editor → Parent (salientes):
| Message Type | Descripción | Payload |
|---|---|---|
SCHEMA_CHANGED |
Notifica cuando el esquema tiene cambios sin guardar | { hasChanges: boolean } |
SCHEMA_UPDATE_SUCCESS |
Metadatos del esquema guardados correctamente | - |
SCHEMA_UPDATE_ERROR |
Falló el guardado de los metadatos del esquema | - |
SEATMAP_UPDATE_SUCCESS |
Gráficos/formas del recinto guardados correctamente | - |
SEATMAP_UPDATE_ERROR |
Falló el guardado de los gráficos/formas del recinto | { payload: unknown } |
UPDATE_PRICE_ASSIGNMENTS_SUCCESS |
Asignaciones de precios guardadas correctamente | - |
UPDATE_PRICE_ASSIGNMENTS_ERROR |
Falló el guardado de las asignaciones de precios | { payload: unknown } |
PRICES_CHANGED |
Las asignaciones de precios tienen cambios sin guardar | { hasPriceChanges: boolean } |
PRICE_CREATED |
Se creó un precio en la lista de precios del evento | { price: { id, name } } |
PRICE_UPDATED |
Se renombró un precio en la lista de precios del evento | { price: { id, name } } |
PRICE_DELETED |
Se eliminó un precio de la lista de precios del evento | { priceId: number } |
PUBLISH_REQUEST |
El usuario hizo clic en el botón de publicar | { payload: ISchemaSettings } |
SVG_RESPONSE |
Respuesta a la solicitud FETCH_SVG (éxito o error) | { success, svg?, error? } |
Parent → Editor (mensajes entrantes)
Mensajes enviados desde la ventana padre al iframe del editor.
SAVE_REQUEST
Activa el guardado del esquema actual por parte del editor. Esta operación persiste todos los cambios realizados en el diseño del recinto, incluyendo asientos, secciones, filas, zonas de precios y gráficos de fondo.
Qué se guarda:
- Diseño y estructura del recinto
- Posiciones, etiquetas y propiedades de los asientos
- Definiciones y límites de las secciones
- Configuraciones y numeración de las filas
- Asignaciones de zonas de precios
- SVG/imágenes de fondo y su posicionamiento
- Formas y etiquetas personalizadas
- Ajustes del viewbox del cliente
Formato:
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
Formato legacy (obsoleto):
iframe.contentWindow.postMessage('SAVE', '*');
Mensajes de respuesta:
La operación de guardado puede activar varios mensajes de respuesta según lo que se haya guardado:
SCHEMA_UPDATE_SUCCESS- Metadatos y ajustes del esquema guardados correctamenteSCHEMA_UPDATE_ERROR- Falló el guardado del esquemaSEATMAP_UPDATE_SUCCESS- Gráficos/formas del recinto guardados correctamenteSEATMAP_UPDATE_ERROR- Falló el guardado de los gráficos/formas del recintoUPDATE_PRICE_ASSIGNMENTS_SUCCESS- Asignaciones de precios guardadas correctamenteUPDATE_PRICE_ASSIGNMENTS_ERROR- Falló el guardado de las asignaciones de precios
Ejemplo básico de guardado:
const iframe = document.getElementById('seatmap-editor');
// Send save request
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
// Listen for response
window.addEventListener('message', (event) => {
if (event.data.type === 'SCHEMA_UPDATE_SUCCESS') {
console.log('Schema saved successfully!');
// Proceed with next action
} else if (event.data.type === 'SCHEMA_UPDATE_ERROR') {
console.error('Failed to save schema');
}
});
Ejemplo completo de manejador de guardado:
let saveInProgress = false;
let saveResults = {
schema: null,
seatmap: null,
prices: null,
};
function handleSave() {
if (saveInProgress) {
console.warn('Save already in progress');
return;
}
saveInProgress = true;
saveResults = { schema: null, seatmap: null, prices: null };
const iframe = document.getElementById('seatmap-editor');
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
// Set timeout to detect save completion
setTimeout(checkSaveCompletion, 5000);
}
function checkSaveCompletion() {
if (!saveInProgress) return;
const allCompleted =
saveResults.schema !== null && saveResults.seatmap !== null && saveResults.prices !== null;
if (allCompleted) {
const allSuccess = saveResults.schema && saveResults.seatmap && saveResults.prices;
if (allSuccess) {
console.log('✓ All changes saved successfully');
onSaveComplete(true);
} else {
console.error('✗ Some save operations failed');
onSaveComplete(false);
}
saveInProgress = false;
}
}
window.addEventListener('message', (event) => {
if (!saveInProgress) return;
switch (event.data.type) {
case 'SCHEMA_UPDATE_SUCCESS':
saveResults.schema = true;
checkSaveCompletion();
break;
case 'SCHEMA_UPDATE_ERROR':
saveResults.schema = false;
checkSaveCompletion();
break;
case 'SEATMAP_UPDATE_SUCCESS':
saveResults.seatmap = true;
checkSaveCompletion();
break;
case 'SEATMAP_UPDATE_ERROR':
saveResults.seatmap = false;
checkSaveCompletion();
break;
case 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS':
saveResults.prices = true;
checkSaveCompletion();
break;
case 'UPDATE_PRICE_ASSIGNMENTS_ERROR':
saveResults.prices = false;
checkSaveCompletion();
break;
}
});
function onSaveComplete(success) {
if (success) {
showNotification('Changes saved successfully', 'success');
// Enable other actions that require saved state
document.getElementById('export-btn').disabled = false;
} else {
showNotification('Failed to save some changes', 'error');
}
}
Guardado con indicador de carga:
function saveWithFeedback() {
const iframe = document.getElementById('seatmap-editor');
const saveBtn = document.getElementById('save-btn');
const spinner = document.getElementById('spinner');
// Show loading state
saveBtn.disabled = true;
spinner.style.display = 'inline-block';
// Send save request
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
// Set timeout for UI feedback
const timeout = setTimeout(() => {
saveBtn.disabled = false;
spinner.style.display = 'none';
showNotification('Save operation timed out', 'warning');
}, 30000); // 30 second timeout
// Track save responses
const listener = (event) => {
if (event.data.type === 'SCHEMA_UPDATE_SUCCESS') {
clearTimeout(timeout);
saveBtn.disabled = false;
spinner.style.display = 'none';
showNotification('Saved successfully!', 'success');
window.removeEventListener('message', listener);
} else if (event.data.type === 'SCHEMA_UPDATE_ERROR') {
clearTimeout(timeout);
saveBtn.disabled = false;
spinner.style.display = 'none';
showNotification('Save failed', 'error');
window.removeEventListener('message', listener);
}
};
window.addEventListener('message', listener);
}
Implementación de autoguardado:
let autoSaveTimeout = null;
let hasUnsavedChanges = false;
window.addEventListener('message', (event) => {
// Track unsaved changes
if (event.data.type === 'SCHEMA_CHANGED') {
hasUnsavedChanges = event.data.hasChanges;
if (hasUnsavedChanges) {
// Debounce auto-save
clearTimeout(autoSaveTimeout);
autoSaveTimeout = setTimeout(() => {
autoSave();
}, 5000); // Auto-save after 5 seconds of inactivity
}
}
});
function autoSave() {
if (!hasUnsavedChanges) return;
console.log('Auto-saving...');
const iframe = document.getElementById('seatmap-editor');
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
}
// Save before page unload
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = 'You have unsaved changes. Do you want to leave?';
}
});
FETCH_SVG
Solicita la exportación SVG del esquema actual. El esquema debe estar guardado antes de la exportación.
Formato:
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
Mensaje de respuesta:
SVG_RESPONSE- Contiene el estado de éxito, los datos SVG o un mensaje de error
Ejemplo:
const iframe = document.getElementById('seatmap-editor');
// Request SVG export
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
// Listen for response
window.addEventListener('message', (event) => {
if (event.data.type === 'SVG_RESPONSE') {
if (event.data.success) {
const svgContent = event.data.svg;
// Use SVG data (download, display, etc.)
downloadSvg(svgContent);
} else {
console.error('SVG export failed:', event.data.error);
// Possible errors:
// - "Schema must be saved before export"
// - Export API errors
}
}
});
function downloadSvg(svgContent) {
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'venue.svg';
link.click();
URL.revokeObjectURL(url);
}
SET_LANGUAGE
Cambia el idioma de la interfaz del editor en tiempo de ejecución. Úsalo para mantener el editor incrustado sincronizado con el idioma de tu aplicación anfitriona sin recargar el iframe.
Formato:
iframe.contentWindow.postMessage({ type: 'SET_LANGUAGE', language: 'pl' }, '*');
Códigos de idioma admitidos: en, ru, de, es, fr, it, zh, pl. Los códigos no admitidos o mal formados se ignoran y se conserva el idioma actual.
Para fijar el idioma cuando el editor carga por primera vez (antes de cualquier interacción del usuario), añade el parámetro de consulta language a la URL del iframe:
<iframe src="https://editor.seatmap.pro/app?language=pl" width="100%" height="800px"></iframe>
Ejemplo:
const iframe = document.getElementById('seatmap-editor');
// Cambiar el editor a polaco
iframe.contentWindow.postMessage({ type: 'SET_LANGUAGE', language: 'pl' }, '*');
Editor → Parent (mensajes salientes)
Mensajes enviados desde el iframe del editor a la ventana padre.
SCHEMA_CHANGED
Notifica al padre cuando el esquema tiene cambios sin guardar.
Payload:
{
type: 'SCHEMA_CHANGED',
hasChanges: boolean
}
Ejemplo:
window.addEventListener('message', (event) => {
if (event.data.type === 'SCHEMA_CHANGED') {
if (event.data.hasChanges) {
// Show unsaved changes indicator
document.getElementById('save-indicator').style.display = 'block';
} else {
// Hide unsaved changes indicator
document.getElementById('save-indicator').style.display = 'none';
}
}
});
SCHEMA_UPDATE_SUCCESS
Se envía después de que una operación de guardado del esquema se completa correctamente.
Payload:
{
type: 'SCHEMA_UPDATE_SUCCESS';
}
SCHEMA_UPDATE_ERROR
Se envía cuando falla una operación de guardado del esquema.
Payload:
{
type: 'SCHEMA_UPDATE_ERROR';
}
SEATMAP_UPDATE_SUCCESS
Se envía después de que los gráficos/formas del recinto se guardan correctamente.
Payload:
{
type: 'SEATMAP_UPDATE_SUCCESS';
}
SEATMAP_UPDATE_ERROR
Se envía cuando falla el guardado de los gráficos/formas del recinto.
Payload:
{
type: 'SEATMAP_UPDATE_ERROR';
}
SVG_RESPONSE
Respuesta a una solicitud FETCH_SVG que contiene el SVG exportado o información de error.
Payload de éxito:
{
type: 'SVG_RESPONSE',
success: true,
svg: string // SVG content as string
}
Payload de error:
{
type: 'SVG_RESPONSE',
success: false,
error: string // Error message
}
Mensajes de error comunes:
"Schema must be saved before export"- El esquema aún no se ha guardado- Mensajes de error de exportación localizados provenientes de la API
UPDATE_PRICE_ASSIGNMENTS_SUCCESS
Se envía después de que las actualizaciones de asignaciones de precios se completan correctamente.
Payload:
{
type: 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS';
}
UPDATE_PRICE_ASSIGNMENTS_ERROR
Se envía cuando fallan las actualizaciones de asignaciones de precios.
Payload:
{
type: 'UPDATE_PRICE_ASSIGNMENTS_ERROR',
payload: unknown // Error details
}
PRICES_CHANGED
Notifica a la ventana padre cuando las asignaciones de precios tienen cambios sin guardar, de forma análoga a SCHEMA_CHANGED para los precios. Se envía con hasPriceChanges: true cuando el usuario asigna o quita precios de asientos o secciones, y con hasPriceChanges: false una vez que las asignaciones se guardan.
Payload:
{
type: 'PRICES_CHANGED',
hasPriceChanges: boolean
}
PRICE_CREATED
Se envía inmediatamente después de crear un nuevo precio en la lista de precios del evento.
A diferencia de las asignaciones de precios, la lista de precios pertenece al evento y se persiste de inmediato: crear, renombrar o eliminar un precio llama al backend al instante y no espera a la acción Save del esquema. Los mensajes PRICE_CREATED, PRICE_UPDATED y PRICE_DELETED permiten a la aplicación padre mantener su propio catálogo de precios sincronizado con estos cambios inmediatos.
Payload:
{
type: 'PRICE_CREATED',
price: {
id: number,
name: string
}
}
Ejemplo:
window.addEventListener('message', (event) => {
switch (event.data.type) {
case 'PRICE_CREATED':
console.log('Price created:', event.data.price.id, event.data.price.name);
break;
case 'PRICE_UPDATED':
console.log('Price renamed:', event.data.price.id, event.data.price.name);
break;
case 'PRICE_DELETED':
console.log('Price deleted:', event.data.priceId);
break;
}
});
PRICE_UPDATED
Se envía inmediatamente después de renombrar un precio existente en la lista de precios del evento.
Payload:
{
type: 'PRICE_UPDATED',
price: {
id: number,
name: string
}
}
PRICE_DELETED
Se envía inmediatamente después de eliminar un precio de la lista de precios del evento.
Payload:
{
type: 'PRICE_DELETED',
priceId: number
}
PUBLISH_REQUEST
Se envía cuando el usuario hace clic en el botón “Publish” del editor. Esto indica que el esquema se está publicando del modo borrador al modo en vivo.
Payload:
{
type: 'PUBLISH_REQUEST',
payload: ISchemaSettings // Schema settings with draft: false
}
Ejemplo:
window.addEventListener('message', (event) => {
if (event.data.type === 'PUBLISH_REQUEST') {
console.log('Schema is being published');
console.log('Schema settings:', event.data.payload);
// Parent application might want to:
// - Show publish confirmation notification
// - Update UI to reflect published status
// - Trigger additional workflows (notifications, webhooks, etc.)
}
});
Notas importantes:
- Este mensaje se envía cuando el usuario inicia la publicación, pero antes de que se complete la operación en el servidor
- El esquema se guardará igualmente a través del flujo de guardado normal, activando
SCHEMA_UPDATE_SUCCESSoSCHEMA_UPDATE_ERROR - El padre debe esperar a
SCHEMA_UPDATE_SUCCESSpara confirmar que la operación de publicación se completó correctamente
Ejemplo de flujo de publicación:
let publishInProgress = false;
window.addEventListener('message', (event) => {
switch (event.data.type) {
case 'PUBLISH_REQUEST':
publishInProgress = true;
console.log('Publishing schema...');
showNotification('Publishing schema...', 'info');
break;
case 'SCHEMA_UPDATE_SUCCESS':
if (publishInProgress) {
publishInProgress = false;
console.log('Schema published successfully!');
showNotification('Schema published successfully!', 'success');
// Trigger post-publish workflows
onSchemaPublished(event.data);
}
break;
case 'SCHEMA_UPDATE_ERROR':
if (publishInProgress) {
publishInProgress = false;
console.error('Failed to publish schema');
showNotification('Failed to publish schema', 'error');
}
break;
}
});
Ejemplo de integración completo
<!DOCTYPE html>
<html>
<head>
<title>Seatmap Editor Integration</title>
<style>
#container {
display: flex;
flex-direction: column;
height: 100vh;
}
#controls {
padding: 10px;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
button {
margin-right: 10px;
padding: 8px 16px;
}
.indicator {
display: none;
color: orange;
margin-left: 10px;
}
.indicator.visible {
display: inline;
}
#editor {
flex: 1;
border: none;
}
#log {
height: 150px;
overflow-y: auto;
padding: 10px;
background: #fafafa;
border-top: 1px solid #ddd;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div id="container">
<div id="controls">
<button id="save-btn">Save Schema</button>
<button id="export-svg-btn">Export SVG</button>
<span id="unsaved-indicator" class="indicator">● Unsaved changes</span>
</div>
<iframe
id="editor"
src="https://editor.seatmap.pro/app?token=YOUR_TOKEN&hidden=save"
></iframe>
<div id="log"></div>
</div>
<script>
const iframe = document.getElementById('editor');
const logEl = document.getElementById('log');
const indicator = document.getElementById('unsaved-indicator');
function log(message) {
const time = new Date().toLocaleTimeString();
logEl.innerHTML += `[${time}] ${message}<br>`;
logEl.scrollTop = logEl.scrollHeight;
}
// Listen for messages from iframe
window.addEventListener('message', (event) => {
// In production, verify origin:
// if (event.origin !== 'https://editor.seatmap.pro') return;
const data = event.data;
log(`Received: ${JSON.stringify(data)}`);
switch (data.type) {
case 'SCHEMA_CHANGED':
if (data.hasChanges) {
indicator.classList.add('visible');
} else {
indicator.classList.remove('visible');
}
break;
case 'SCHEMA_UPDATE_SUCCESS':
log('✓ Schema saved successfully');
break;
case 'SCHEMA_UPDATE_ERROR':
log('✗ Schema save failed');
break;
case 'SEATMAP_UPDATE_SUCCESS':
log('✓ Venue graphics saved successfully');
break;
case 'SEATMAP_UPDATE_ERROR':
log('✗ Venue graphics save failed');
break;
case 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS':
log('✓ Price assignments saved successfully');
break;
case 'UPDATE_PRICE_ASSIGNMENTS_ERROR':
log('✗ Price assignments save failed');
break;
case 'PUBLISH_REQUEST':
log('📤 Schema is being published...');
break;
case 'SVG_RESPONSE':
if (data.success) {
log('✓ SVG export successful');
downloadSvg(data.svg);
} else {
log(`✗ SVG export failed: ${data.error}`);
alert(data.error);
}
break;
}
});
// Save button
document.getElementById('save-btn').addEventListener('click', () => {
log('Sending SAVE_REQUEST...');
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
});
// Export SVG button
document.getElementById('export-svg-btn').addEventListener('click', () => {
log('Sending FETCH_SVG...');
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
});
function downloadSvg(svgContent) {
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `venue_${Date.now()}.svg`;
link.click();
URL.revokeObjectURL(url);
log('SVG downloaded');
}
</script>
</body>
</html>
Manejo de errores
Escenarios comunes
Esquema no guardado
Al solicitar la exportación SVG sin guardar primero:
// Response:
{
type: 'SVG_RESPONSE',
success: false,
error: 'Schema must be saved before export'
}
Solución: Activa primero un guardado y luego solicita el SVG:
function saveAndExport() {
const iframe = document.getElementById('seatmap-editor');
// Send save request
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
// Wait for save confirmation
const listener = (event) => {
if (event.data.type === 'SCHEMA_UPDATE_SUCCESS') {
// Now request SVG
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
window.removeEventListener('message', listener);
} else if (event.data.type === 'SCHEMA_UPDATE_ERROR') {
console.error('Save failed, cannot export SVG');
alert('Please save the schema before exporting');
window.removeEventListener('message', listener);
}
};
window.addEventListener('message', listener);
}
Flujo completo de «guardar y luego exportar»:
function saveAndExportWithFeedback() {
const iframe = document.getElementById('seatmap-editor');
const exportBtn = document.getElementById('export-btn');
exportBtn.disabled = true;
exportBtn.textContent = 'Saving...';
// Step 1: Save the schema
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
let saveCompleted = false;
const messageHandler = (event) => {
// Handle save response
if (!saveCompleted) {
if (event.data.type === 'SCHEMA_UPDATE_SUCCESS') {
saveCompleted = true;
exportBtn.textContent = 'Exporting...';
// Step 2: Request SVG export
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
} else if (event.data.type === 'SCHEMA_UPDATE_ERROR') {
exportBtn.disabled = false;
exportBtn.textContent = 'Export SVG';
alert('Failed to save schema. Cannot export.');
window.removeEventListener('message', messageHandler);
}
}
// Handle export response
else {
if (event.data.type === 'SVG_RESPONSE') {
exportBtn.disabled = false;
exportBtn.textContent = 'Export SVG';
if (event.data.success) {
console.log('Export successful');
downloadSvg(event.data.svg);
} else {
alert('Export failed: ' + event.data.error);
}
window.removeEventListener('message', messageHandler);
}
}
};
window.addEventListener('message', messageHandler);
// Timeout handler
setTimeout(() => {
exportBtn.disabled = false;
exportBtn.textContent = 'Export SVG';
window.removeEventListener('message', messageHandler);
}, 60000); // 60 second timeout
}
function downloadSvg(svgContent) {
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `venue_${Date.now()}.svg`;
link.click();
URL.revokeObjectURL(url);
}
Errores de API
Errores de red o del servidor durante la exportación:
{
type: 'SVG_RESPONSE',
success: false,
error: 'Export failed' // Localized error message
}
Solución: Muestra un mensaje de error comprensible para el usuario y ofrece la opción de reintentar.
Consideraciones sobre el tamaño de los mensajes
Las exportaciones SVG pueden ser grandes, especialmente para recintos complejos:
- Recintos pequeños: 10KB - 100KB
- Recintos medianos: 100KB - 1MB
- Recintos grandes: 1MB - 10MB+
La API postMessage admite mensajes de hasta ~64MB en los navegadores modernos, pero las cargas útiles muy grandes pueden causar problemas de rendimiento. Para recintos con gráficos extremadamente complejos, considera:
- Usar el endpoint directo de la API (
/api/export/${schemaId}/) en su lugar - Descargar mediante procesamiento del lado del servidor
- Implementar indicadores de progreso para el usuario
Pruebas
Archivos de prueba
El repositorio incluye archivos HTML de prueba para desarrollo y depuración:
-
public/sso_test.html- Página de prueba con todas las funciones:- Flujo de autenticación SSO
- Embebido del iframe en una ventana modal
- Botón de solicitud de guardado
- Panel de registro de mensajes
- Visualización de mensajes en tiempo real
-
public/iframe_modal_test.html- Prueba modal simple con:- Embebido básico del iframe
- Registro de eventos de mensaje
- Seguimiento de cambios del esquema
Pruebas locales
# Start development server
yarn start
# Open test page
open http://localhost:3000/sso_test.html
Consideraciones de seguridad
Verificación del origin
Verifica siempre el origin de los mensajes recibidos en producción:
window.addEventListener('message', (event) => {
// Verify the origin
const allowedOrigins = ['https://editor.seatmap.pro', 'https://editor.seatmap.dev'];
if (!allowedOrigins.includes(event.origin)) {
console.warn('Message from untrusted origin:', event.origin);
return;
}
// Process message
handleMessage(event.data);
});
Origin de destino
Al enviar mensajes al iframe, especifica el origin de destino:
// Instead of '*', use specific origin
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, 'https://editor.seatmap.pro');
Autenticación
El editor requiere autenticación JWT. Pasa los tokens a través de los parámetros de la URL:
https://editor.seatmap.pro/app?token=JWT_TOKEN&refreshToken=REFRESH_TOKEN
Asegúrate de que los tokens se transmitan de forma segura y tengan tiempos de expiración apropiados.
Resolución de problemas
Los mensajes no se reciben
-
Comprueba que el iframe está cargado:
iframe.addEventListener('load', () => { console.log('Iframe loaded, ready for messages'); }); -
Verifica el acceso a contentWindow:
if (!iframe.contentWindow) { console.error('Cannot access iframe contentWindow'); } -
Comprueba las restricciones de cross-origin:
- Asegúrate de que las cabeceras CORS estén correctamente configuradas
- Usa la consola del navegador para comprobar errores de seguridad
El guardado no funciona
Causas comunes:
-
Problemas de autenticación
- Token expirado o inválido
- El usuario no tiene permisos de escritura
- La sesión expiró
-
Errores de validación
- Los datos del esquema están incompletos o son inválidos
- Faltan campos obligatorios
- Errores de formato de datos
-
Problemas de red
- El endpoint de la API es inaccesible
- Restricciones de CORS
- El firewall bloquea las solicitudes
-
Problemas de estado
- No hay cambios para guardar
- El editor no está completamente cargado
- Operaciones de guardado concurrentes
Pasos de depuración:
// Check if editor is loaded
iframe.addEventListener('load', () => {
console.log('Editor iframe loaded');
});
// Monitor all messages
window.addEventListener('message', (event) => {
console.log('Message received:', event.data);
});
// Check for errors in browser console
// Check Network tab for failed API calls
// Verify authentication token is present in URL
Lista de verificación de la operación de guardado:
- El iframe del editor está completamente cargado
- El usuario está autenticado (token presente)
- El usuario tiene permisos de edición para el recinto
- El esquema tiene cambios sin guardar (escucha
SCHEMA_CHANGED) - No hay otra operación de guardado en curso
- La conectividad de red es estable
Falla la exportación SVG
Causas comunes:
- El esquema no está guardado (la más común)
- Problemas de conectividad de red
- Errores de exportación del lado del servidor
- Datos del esquema inválidos
Diagrama de flujo de mensajes
Flujo de la operación de guardado
Parent sends SAVE_REQUEST
↓
Editor begins save operation
↓
Editor sends SCHEMA_CHANGED (hasChanges: false)
↓
Editor saves schema metadata → SCHEMA_UPDATE_SUCCESS or SCHEMA_UPDATE_ERROR
↓
Editor saves venue graphics → SEATMAP_UPDATE_SUCCESS or SEATMAP_UPDATE_ERROR
↓
Editor saves price assignments → UPDATE_PRICE_ASSIGNMENTS_SUCCESS or UPDATE_PRICE_ASSIGNMENTS_ERROR
↓
Save operation complete
Flujo de la operación de publicación
User clicks "Publish" button in editor
↓
Editor sends PUBLISH_REQUEST (with payload: { draft: false, ... })
↓
Editor triggers save operation (same flow as above)
↓
SCHEMA_UPDATE_SUCCESS indicates publish completed successfully
Flujo de exportación SVG
Parent sends FETCH_SVG
↓
Editor checks if schema is saved
↓
If not saved: SVG_RESPONSE (success: false, error: "Schema must be saved before export")
↓
If saved: Editor fetches SVG from API
↓
SVG_RESPONSE (success: true/false, svg or error)
Flujos de trabajo comunes
Manejador de mensajes integral
class SeatmapEditorIntegration {
constructor(iframeId) {
this.iframe = document.getElementById(iframeId);
this.hasUnsavedChanges = false;
this.saveInProgress = false;
this.publishInProgress = false;
this.saveResults = { schema: null, seatmap: null, prices: null };
this.initMessageListener();
}
initMessageListener() {
window.addEventListener('message', (event) => {
// Verify origin in production
// if (event.origin !== 'https://editor.seatmap.pro') return;
this.handleMessage(event.data);
});
}
handleMessage(data) {
console.log('Received message:', data);
switch (data.type) {
case 'SCHEMA_CHANGED':
this.hasUnsavedChanges = data.hasChanges;
this.updateUI();
break;
case 'SCHEMA_UPDATE_SUCCESS':
this.saveResults.schema = true;
this.checkSaveCompletion();
break;
case 'SCHEMA_UPDATE_ERROR':
this.saveResults.schema = false;
this.checkSaveCompletion();
break;
case 'SEATMAP_UPDATE_SUCCESS':
this.saveResults.seatmap = true;
this.checkSaveCompletion();
break;
case 'SEATMAP_UPDATE_ERROR':
this.saveResults.seatmap = false;
this.checkSaveCompletion();
break;
case 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS':
this.saveResults.prices = true;
this.checkSaveCompletion();
break;
case 'UPDATE_PRICE_ASSIGNMENTS_ERROR':
this.saveResults.prices = false;
this.checkSaveCompletion();
break;
case 'PUBLISH_REQUEST':
console.log('Schema is being published');
this.publishInProgress = true;
this.showNotification('Publishing schema...', 'info');
break;
case 'SVG_RESPONSE':
this.handleSvgResponse(data);
break;
default:
console.log('Unknown message type:', data.type);
}
}
checkSaveCompletion() {
if (!this.saveInProgress) return;
const { schema, seatmap, prices } = this.saveResults;
if (schema === null || seatmap === null || prices === null) return;
this.saveInProgress = false;
const success = schema && seatmap && prices;
if (success) {
this.hasUnsavedChanges = false;
this.showNotification(
this.publishInProgress ? 'Published successfully!' : 'Saved successfully!',
'success',
);
this.onSaveComplete(true);
} else {
this.showNotification('Save failed', 'error');
this.onSaveComplete(false);
}
this.publishInProgress = false;
}
save() {
if (this.saveInProgress) {
console.warn('Save already in progress');
return;
}
this.saveInProgress = true;
this.saveResults = { schema: null, seatmap: null, prices: null };
this.iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
}
exportSvg() {
this.iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
}
handleSvgResponse(data) {
if (data.success) {
this.downloadFile(data.svg, 'venue.svg', 'image/svg+xml');
this.showNotification('SVG exported successfully', 'success');
} else {
this.showNotification(`Export failed: ${data.error}`, 'error');
}
}
downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
updateUI() {
const indicator = document.getElementById('unsaved-indicator');
if (indicator) {
indicator.style.display = this.hasUnsavedChanges ? 'inline' : 'none';
}
}
showNotification(message, type) {
console.log(`[${type}] ${message}`);
// Implement your notification system here
}
onSaveComplete(success) {
// Override this method to handle save completion
console.log('Save completed:', success);
}
}
// Usage
const editor = new SeatmapEditorIntegration('seatmap-editor');
document.getElementById('save-btn').addEventListener('click', () => {
editor.save();
});
document.getElementById('export-btn').addEventListener('click', () => {
editor.exportSvg();
});
// Warn before leaving with unsaved changes
window.addEventListener('beforeunload', (event) => {
if (editor.hasUnsavedChanges) {
event.preventDefault();
event.returnValue = 'You have unsaved changes. Do you want to leave?';
}
});
Guardar y cerrar la ventana modal
function saveAndClose() {
const iframe = document.getElementById('seatmap-editor');
const modal = document.getElementById('editor-modal');
// Show saving indicator
showStatus('Saving changes...');
// Send save request
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
const listener = (event) => {
if (event.data.type === 'SCHEMA_UPDATE_SUCCESS') {
showStatus('Saved successfully!');
setTimeout(() => {
modal.style.display = 'none';
}, 1000);
window.removeEventListener('message', listener);
} else if (event.data.type === 'SCHEMA_UPDATE_ERROR') {
showStatus('Save failed!', 'error');
// Keep modal open on error
window.removeEventListener('message', listener);
}
};
window.addEventListener('message', listener);
}
// Warn before closing with unsaved changes
let hasUnsavedChanges = false;
window.addEventListener('message', (event) => {
if (event.data.type === 'SCHEMA_CHANGED') {
hasUnsavedChanges = event.data.hasChanges;
}
});
function attemptClose() {
if (hasUnsavedChanges) {
if (confirm('You have unsaved changes. Save before closing?')) {
saveAndClose();
} else {
closeModal();
}
} else {
closeModal();
}
}
Autoguardado periódico con visualización de estado
class EditorManager {
constructor(iframeId) {
this.iframe = document.getElementById(iframeId);
this.hasChanges = false;
this.lastSaveTime = null;
this.autoSaveInterval = 60000; // 1 minute
this.autoSaveTimer = null;
this.initMessageListener();
this.startAutoSave();
}
initMessageListener() {
window.addEventListener('message', (event) => {
this.handleMessage(event.data);
});
}
handleMessage(data) {
switch (data.type) {
case 'SCHEMA_CHANGED':
this.hasChanges = data.hasChanges;
this.updateStatus();
if (data.hasChanges) {
this.scheduleAutoSave();
}
break;
case 'SCHEMA_UPDATE_SUCCESS':
this.hasChanges = false;
this.lastSaveTime = new Date();
this.updateStatus();
break;
case 'SCHEMA_UPDATE_ERROR':
this.showError('Failed to save changes');
break;
}
}
scheduleAutoSave() {
clearTimeout(this.autoSaveTimer);
this.autoSaveTimer = setTimeout(() => {
if (this.hasChanges) {
this.save();
}
}, this.autoSaveInterval);
}
startAutoSave() {
setInterval(() => {
if (this.hasChanges) {
console.log('Auto-saving...');
this.save();
}
}, this.autoSaveInterval);
}
save() {
this.iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
}
updateStatus() {
const statusEl = document.getElementById('save-status');
if (this.hasChanges) {
statusEl.textContent = '● Unsaved changes';
statusEl.className = 'status-warning';
} else if (this.lastSaveTime) {
const elapsed = Math.floor((new Date() - this.lastSaveTime) / 1000);
statusEl.textContent = `✓ Saved ${elapsed}s ago`;
statusEl.className = 'status-success';
} else {
statusEl.textContent = 'No changes';
statusEl.className = 'status-info';
}
}
showError(message) {
const statusEl = document.getElementById('save-status');
statusEl.textContent = `✗ ${message}`;
statusEl.className = 'status-error';
}
}
// Usage
const editor = new EditorManager('seatmap-editor');
// Manual save button
document.getElementById('save-btn').addEventListener('click', () => {
editor.save();
});
Flujo «guardar — exportar — descargar — cerrar»
async function completeWorkflow() {
const iframe = document.getElementById('seatmap-editor');
const statusEl = document.getElementById('workflow-status');
try {
// Step 1: Save
statusEl.textContent = 'Step 1/3: Saving schema...';
await sendMessageAndWait(
iframe,
{ type: 'SAVE_REQUEST' },
'SCHEMA_UPDATE_SUCCESS',
'SCHEMA_UPDATE_ERROR',
);
// Step 2: Export SVG
statusEl.textContent = 'Step 2/3: Exporting SVG...';
const svgData = await sendMessageAndWait(iframe, { type: 'FETCH_SVG' }, null, null, true);
if (!svgData.success) {
throw new Error(svgData.error);
}
// Step 3: Download
statusEl.textContent = 'Step 3/3: Downloading...';
downloadFile(svgData.svg, 'venue.svg', 'image/svg+xml');
// Step 4: Close
statusEl.textContent = 'Complete! Closing...';
setTimeout(() => {
closeEditor();
}, 1000);
} catch (error) {
statusEl.textContent = `Error: ${error.message}`;
console.error('Workflow failed:', error);
}
}
function sendMessageAndWait(iframe, message, successType, errorType, returnData = false) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('Operation timed out'));
}, 30000);
const handler = (event) => {
if (returnData && event.data.type === 'SVG_RESPONSE') {
clearTimeout(timeout);
window.removeEventListener('message', handler);
resolve(event.data);
} else if (successType && event.data.type === successType) {
clearTimeout(timeout);
window.removeEventListener('message', handler);
resolve(event.data);
} else if (errorType && event.data.type === errorType) {
clearTimeout(timeout);
window.removeEventListener('message', handler);
reject(new Error('Operation failed'));
}
};
window.addEventListener('message', handler);
iframe.contentWindow.postMessage(message, '*');
});
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
Operaciones por lotes con seguimiento de progreso
class BatchOperationManager {
constructor(iframeId) {
this.iframe = document.getElementById(iframeId);
this.operations = [];
this.currentOperation = null;
}
addOperation(name, message, successTypes, errorTypes) {
this.operations.push({
name,
message,
successTypes: Array.isArray(successTypes) ? successTypes : [successTypes],
errorTypes: Array.isArray(errorTypes) ? errorTypes : [errorTypes],
status: 'pending',
});
}
async executeAll() {
const totalOps = this.operations.length;
for (let i = 0; i < totalOps; i++) {
const op = this.operations[i];
this.updateProgress(i + 1, totalOps, op.name);
try {
await this.executeOperation(op);
op.status = 'success';
} catch (error) {
op.status = 'failed';
console.error(`Operation "${op.name}" failed:`, error);
throw error; // Stop on first failure
}
}
this.updateProgress(totalOps, totalOps, 'Complete');
}
executeOperation(op) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error(`${op.name} timed out`));
}, 60000);
const handler = (event) => {
if (op.successTypes.includes(event.data.type)) {
clearTimeout(timeout);
window.removeEventListener('message', handler);
resolve(event.data);
} else if (op.errorTypes.includes(event.data.type)) {
clearTimeout(timeout);
window.removeEventListener('message', handler);
reject(new Error(`${op.name} failed`));
}
};
window.addEventListener('message', handler);
this.iframe.contentWindow.postMessage(op.message, '*');
});
}
updateProgress(current, total, currentOp) {
const percent = Math.round((current / total) * 100);
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
if (progressBar) progressBar.style.width = `${percent}%`;
if (progressText) progressText.textContent = `${currentOp} (${current}/${total})`;
}
}
// Usage example
async function saveThenExportThenClose() {
const batch = new BatchOperationManager('seatmap-editor');
batch.addOperation(
'Save Schema',
{ type: 'SAVE_REQUEST' },
'SCHEMA_UPDATE_SUCCESS',
'SCHEMA_UPDATE_ERROR',
);
batch.addOperation('Export SVG', { type: 'FETCH_SVG' }, 'SVG_RESPONSE', 'SVG_RESPONSE');
try {
await batch.executeAll();
console.log('All operations completed successfully');
setTimeout(() => closeEditor(), 1000);
} catch (error) {
alert('Operation failed: ' + error.message);
}
}
Soporte para TypeScript
Definiciones de tipos para las cargas útiles de los mensajes:
// Incoming message types (Parent → Editor)
type IncomingMessage = { type: 'SAVE_REQUEST' } | { type: 'FETCH_SVG' } | 'SAVE'; // Legacy format
// Outgoing message types (Editor → Parent)
type OutgoingMessage =
| { type: 'SCHEMA_CHANGED'; hasChanges: boolean }
| { type: 'SCHEMA_UPDATE_SUCCESS' }
| { type: 'SCHEMA_UPDATE_ERROR' }
| { type: 'SEATMAP_UPDATE_SUCCESS' }
| { type: 'SEATMAP_UPDATE_ERROR' }
| { type: 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS' }
| { type: 'UPDATE_PRICE_ASSIGNMENTS_ERROR'; payload: unknown }
| { type: 'PUBLISH_REQUEST'; payload: ISchemaSettings }
| { type: 'SVG_RESPONSE'; success: true; svg: string }
| { type: 'SVG_RESPONSE'; success: false; error: string };
// Schema settings interface (simplified)
interface ISchemaSettings {
id?: number;
venueId: number;
name: string;
draft: boolean;
// ... other properties
}
// Event listener with types
window.addEventListener('message', (event: MessageEvent<OutgoingMessage>) => {
const data = event.data;
if (data.type === 'SVG_RESPONSE') {
if (data.success) {
const svg: string = data.svg;
} else {
const error: string = data.error;
}
}
});
Buenas prácticas
1. Verifica siempre el origin de los mensajes
window.addEventListener('message', (event) => {
const allowedOrigins = ['https://editor.seatmap.pro', 'https://editor.seatmap.dev'];
if (!allowedOrigins.includes(event.origin)) {
return;
}
handleMessage(event.data);
});
2. Maneja todos los mensajes relacionados con el guardado
Una operación de guardado completa puede activar hasta 3 mensajes de éxito distintos:
let saveResults = { schema: false, seatmap: false, prices: false };
function checkAllSaved() {
return saveResults.schema && saveResults.seatmap && saveResults.prices;
}
3. Implementa timeouts para las operaciones
const timeout = setTimeout(() => {
console.error('Save operation timed out');
resetSaveState();
}, 30000); // 30 second timeout
// Clear timeout when operation completes
4. Realiza un seguimiento del estado de las operaciones
const operationState = {
saveInProgress: false,
publishInProgress: false,
exportInProgress: false,
};
// Prevent duplicate operations
if (operationState.saveInProgress) {
console.warn('Save already in progress');
return;
}
5. Proporciona retroalimentación al usuario
// Show loading indicators
// Display success/error notifications
// Update UI state based on messages
// Log operations for debugging
6. Maneja los cambios sin guardar
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = '';
}
});
7. Espera el guardado del esquema antes de exportar
async function saveAndExport() {
// Save first
await save();
// Then export
exportSvg();
}
Recursos adicionales
- MDN: Window.postMessage()
- Documentación de la API de Seatmap Editor
- Implementación:
src/utils/parentMessaging.ts - Ejemplos de integración:
public/sso_test.html,public/iframe_modal_test.html
Soporte
Para preguntas o problemas con la integración del iframe:
- Consulta los archivos de prueba existentes para ver ejemplos funcionales
- Revisa la consola del navegador en busca de mensajes de error
- Contacta con soporte indicando los detalles específicos del error y la información del navegador