IFrame communications
Iframe Communication API
This document describes the postMessage-based communication protocol for embedding the Seatmap Editor as an iframe in parent applications.
Overview
The Seatmap Editor supports bidirectional communication via the browser’s postMessage API when embedded as an iframe. This enables parent applications to:
- Request the editor to save the current schema
- Fetch SVG export of the current venue
- Receive notifications about schema changes and save status
- Receive notifications when a schema is published
- Monitor save progress for different components (schema metadata, venue graphics, price assignments)
Setup
Embedding the Editor
<iframe
id="seatmap-editor"
src="https://editor.seatmap.pro/app?token=YOUR_JWT_TOKEN"
width="100%"
height="800px"
>
</iframe>
Message Event Listener
Set up a message listener in the parent window to receive messages from the 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);
}
});
Message Types Reference
Summary
Parent → Editor (Incoming):
| Message Type | Description | Legacy Format |
|---|---|---|
SAVE_REQUEST |
Request editor to save current schema | 'SAVE' |
FETCH_SVG |
Request SVG export of current schema | - |
Editor → Parent (Outgoing):
| Message Type | Description | Payload |
|---|---|---|
SCHEMA_CHANGED |
Notifies when schema has unsaved changes | { hasChanges: boolean } |
SCHEMA_UPDATE_SUCCESS |
Schema metadata saved successfully | - |
SCHEMA_UPDATE_ERROR |
Schema metadata save failed | - |
SEATMAP_UPDATE_SUCCESS |
Venue graphics/shapes saved successfully | - |
SEATMAP_UPDATE_ERROR |
Venue graphics/shapes save failed | { payload: unknown } |
UPDATE_PRICE_ASSIGNMENTS_SUCCESS |
Price assignments saved successfully | - |
UPDATE_PRICE_ASSIGNMENTS_ERROR |
Price assignments save failed | { payload: unknown } |
PUBLISH_REQUEST |
User clicked publish button | { payload: ISchemaSettings } |
SVG_RESPONSE |
Response to FETCH_SVG request (success or error) | { success, svg?, error? } |
Parent → Editor (Incoming Messages)
Messages sent from the parent window to the editor iframe.
SAVE_REQUEST
Triggers the editor to save the current schema. This operation persists all changes made to the venue layout, including seats, sections, rows, pricing zones, and background graphics.
What Gets Saved:
- Venue layout and structure
- Seat positions, labels, and properties
- Section definitions and boundaries
- Row configurations and numbering
- Pricing zone assignments
- Background SVG/images and positioning
- Custom shapes and labels
- Client viewbox settings
Format:
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
Legacy Format (deprecated):
iframe.contentWindow.postMessage('SAVE', '*');
Response Messages:
The save operation may trigger multiple response messages depending on what was saved:
SCHEMA_UPDATE_SUCCESS- Schema metadata and settings saved successfullySCHEMA_UPDATE_ERROR- Schema save failedSEATMAP_UPDATE_SUCCESS- Venue graphics/shapes saved successfullySEATMAP_UPDATE_ERROR- Venue graphics/shapes save failedUPDATE_PRICE_ASSIGNMENTS_SUCCESS- Price assignments saved successfullyUPDATE_PRICE_ASSIGNMENTS_ERROR- Price assignments save failed
Basic Save Example:
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');
}
});
Complete Save Handler Example:
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');
}
}
Save with Loading Indicator:
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);
}
Auto-save Implementation:
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
Requests the SVG export of the current schema. The schema must be saved before export.
Format:
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
Response Message:
SVG_RESPONSE- Contains success status, SVG data, or error message
Example:
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);
}
Editor → Parent (Outgoing Messages)
Messages sent from the editor iframe to the parent window.
SCHEMA_CHANGED
Notifies the parent when the schema has unsaved changes.
Payload:
{
type: 'SCHEMA_CHANGED',
hasChanges: boolean
}
Example:
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
Sent after a schema save operation completes successfully.
Payload:
{
type: 'SCHEMA_UPDATE_SUCCESS';
}
SCHEMA_UPDATE_ERROR
Sent when a schema save operation fails.
Payload:
{
type: 'SCHEMA_UPDATE_ERROR';
}
SEATMAP_UPDATE_SUCCESS
Sent after venue graphics/shapes save successfully.
Payload:
{
type: 'SEATMAP_UPDATE_SUCCESS';
}
SEATMAP_UPDATE_ERROR
Sent when venue graphics/shapes save fails.
Payload:
{
type: 'SEATMAP_UPDATE_ERROR';
}
SVG_RESPONSE
Response to a FETCH_SVG request containing the exported SVG or error information.
Success Payload:
{
type: 'SVG_RESPONSE',
success: true,
svg: string // SVG content as string
}
Error Payload:
{
type: 'SVG_RESPONSE',
success: false,
error: string // Error message
}
Common Error Messages:
"Schema must be saved before export"- The schema hasn’t been saved yet- Localized export error messages from the API
UPDATE_PRICE_ASSIGNMENTS_SUCCESS
Sent after price assignment updates complete successfully.
Payload:
{
type: 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS';
}
UPDATE_PRICE_ASSIGNMENTS_ERROR
Sent when price assignment updates fail.
Payload:
{
type: 'UPDATE_PRICE_ASSIGNMENTS_ERROR',
payload: unknown // Error details
}
PUBLISH_REQUEST
Sent when the user clicks the “Publish” button in the editor. This indicates that the schema is being published from draft mode to live mode.
Payload:
{
type: 'PUBLISH_REQUEST',
payload: ISchemaSettings // Schema settings with draft: false
}
Example:
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.)
}
});
Important Notes:
- This message is sent when the user initiates publishing, but before the server operation completes
- The schema will still be saved via the normal save flow, triggering
SCHEMA_UPDATE_SUCCESSorSCHEMA_UPDATE_ERROR - The parent should wait for
SCHEMA_UPDATE_SUCCESSto confirm the publish operation completed successfully
Publish Workflow Example:
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;
}
});
Complete Integration Example
<!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>
Error Handling
Common Scenarios
Schema Not Saved
When requesting SVG export without saving first:
// Response:
{
type: 'SVG_RESPONSE',
success: false,
error: 'Schema must be saved before export'
}
Solution: Trigger a save first, then request 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);
}
Complete Save-then-Export Workflow:
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);
}
API Errors
Network or server errors during export:
{
type: 'SVG_RESPONSE',
success: false,
error: 'Export failed' // Localized error message
}
Solution: Show user-friendly error message and provide retry option.
Message Size Considerations
SVG exports can be large, especially for complex venues:
- Small venues: 10KB - 100KB
- Medium venues: 100KB - 1MB
- Large venues: 1MB - 10MB+
The postMessage API supports messages up to ~64MB in modern browsers, but very large payloads may cause performance issues. For venues with extremely complex graphics, consider:
- Using the direct API endpoint (
/api/export/${schemaId}/) instead - Downloading via server-side processing
- Implementing progress indicators for the user
Testing
Test Files
The repository includes test HTML files for development and debugging:
-
public/sso_test.html- Full-featured test page with:- SSO authentication flow
- Modal iframe embedding
- Save request button
- Message logging panel
- Real-time message display
-
public/iframe_modal_test.html- Simple modal test with:- Basic iframe embedding
- Message event logging
- Schema change tracking
Local Testing
# Start development server
yarn start
# Open test page
open http://localhost:3000/sso_test.html
Security Considerations
Origin Verification
Always verify the origin of received messages in production:
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);
});
Target Origin
When sending messages to the iframe, specify the target origin:
// Instead of '*', use specific origin
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, 'https://editor.seatmap.pro');
Authentication
The editor requires JWT authentication. Pass tokens via URL parameters:
https://editor.seatmap.pro/app?token=JWT_TOKEN&refreshToken=REFRESH_TOKEN
Ensure tokens are transmitted securely and have appropriate expiration times.
Troubleshooting
Messages Not Being Received
-
Check iframe is loaded:
iframe.addEventListener('load', () => { console.log('Iframe loaded, ready for messages'); }); -
Verify contentWindow access:
if (!iframe.contentWindow) { console.error('Cannot access iframe contentWindow'); } -
Check for cross-origin restrictions:
- Ensure proper CORS headers are set
- Use browser console to check for security errors
Save Not Working
Common Causes:
-
Authentication Issues
- Token expired or invalid
- User doesn’t have write permissions
- Session timed out
-
Validation Errors
- Schema data is incomplete or invalid
- Required fields are missing
- Data format errors
-
Network Issues
- API endpoint unreachable
- CORS restrictions
- Firewall blocking requests
-
State Issues
- No changes to save
- Editor not fully loaded
- Concurrent save operations
Debugging Steps:
// 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
Save Operation Checklist:
- Editor iframe is fully loaded
- User is authenticated (token present)
- User has edit permissions for the venue
- Schema has unsaved changes (listen for
SCHEMA_CHANGED) - No other save operation in progress
- Network connectivity is stable
SVG Export Fails
Common causes:
- Schema not saved (most common)
- Network connectivity issues
- Server-side export errors
- Invalid schema data
Message Flow Diagram
Save Operation Flow
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
Publish Operation Flow
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
SVG Export Flow
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)
Common Workflows
Comprehensive Message Handler
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?';
}
});
Save and Close 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();
}
}
Periodic Auto-Save with Status Display
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();
});
Save-Export-Download-Close Workflow
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);
}
Batch Operations with Progress Tracking
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);
}
}
TypeScript Support
Type definitions for message payloads:
// 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;
}
}
});
Best Practices
1. Always Verify Message Origins
window.addEventListener('message', (event) => {
const allowedOrigins = ['https://editor.seatmap.pro', 'https://editor.seatmap.dev'];
if (!allowedOrigins.includes(event.origin)) {
return;
}
handleMessage(event.data);
});
2. Handle All Save-Related Messages
A complete save operation can trigger up to 3 different success messages:
let saveResults = { schema: false, seatmap: false, prices: false };
function checkAllSaved() {
return saveResults.schema && saveResults.seatmap && saveResults.prices;
}
3. Implement Timeouts for Operations
const timeout = setTimeout(() => {
console.error('Save operation timed out');
resetSaveState();
}, 30000); // 30 second timeout
// Clear timeout when operation completes
4. Track Operation State
const operationState = {
saveInProgress: false,
publishInProgress: false,
exportInProgress: false,
};
// Prevent duplicate operations
if (operationState.saveInProgress) {
console.warn('Save already in progress');
return;
}
5. Provide User Feedback
// Show loading indicators
// Display success/error notifications
// Update UI state based on messages
// Log operations for debugging
6. Handle Unsaved Changes
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = '';
}
});
7. Wait for Schema Save Before Export
async function saveAndExport() {
// Save first
await save();
// Then export
exportSvg();
}
Further Resources
- MDN: Window.postMessage()
- Seatmap Editor API Documentation
- Implementation:
src/utils/parentMessaging.ts - Integration examples:
public/sso_test.html,public/iframe_modal_test.html
Support
For questions or issues with iframe integration:
- Check existing test files for working examples
- Review browser console for error messages
- Contact support with specific error details and browser information