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 successfully
  • SCHEMA_UPDATE_ERROR - Schema save failed
  • SEATMAP_UPDATE_SUCCESS - Venue graphics/shapes saved successfully
  • SEATMAP_UPDATE_ERROR - Venue graphics/shapes save failed
  • UPDATE_PRICE_ASSIGNMENTS_SUCCESS - Price assignments saved successfully
  • UPDATE_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_SUCCESS or SCHEMA_UPDATE_ERROR
  • The parent should wait for SCHEMA_UPDATE_SUCCESS to 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:

  1. 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
  2. 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

  1. Check iframe is loaded:

    iframe.addEventListener('load', () => {
      console.log('Iframe loaded, ready for messages');
    });
    
  2. Verify contentWindow access:

    if (!iframe.contentWindow) {
      console.error('Cannot access iframe contentWindow');
    }
    
  3. Check for cross-origin restrictions:

    • Ensure proper CORS headers are set
    • Use browser console to check for security errors

Save Not Working

Common Causes:

  1. Authentication Issues

    • Token expired or invalid
    • User doesn’t have write permissions
    • Session timed out
  2. Validation Errors

    • Schema data is incomplete or invalid
    • Required fields are missing
    • Data format errors
  3. Network Issues

    • API endpoint unreachable
    • CORS restrictions
    • Firewall blocking requests
  4. 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);
});

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

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