Коммуникация через IFrame
API коммуникации через Iframe
Этот документ описывает протокол коммуникации на базе postMessage для встраивания Seatmap Editor в виде iframe в родительские приложения.
Обзор
Seatmap Editor поддерживает двунаправленную коммуникацию через браузерный API postMessage, когда встроен в виде iframe. Это позволяет родительским приложениям:
- Запрашивать у редактора сохранение текущей схемы
- Получать SVG-экспорт текущей площадки
- Получать уведомления об изменениях схемы и статусе сохранения
- Получать уведомления о публикации схемы
- Отслеживать прогресс сохранения для разных компонентов (метаданные схемы, графика площадки, назначения цен)
Настройка
Встраивание Editor
<iframe
id="seatmap-editor"
src="https://editor.seatmap.pro/app?token=YOUR_JWT_TOKEN"
width="100%"
height="800px"
>
</iframe>
Обработчик событий сообщений
Настройте в родительском окне обработчик сообщений для получения сообщений от редактора:
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);
}
});
Справочник по типам сообщений
Сводка
Parent → Editor (входящие):
| Message Type | Описание | Legacy Format |
|---|---|---|
SAVE_REQUEST |
Запрос редактору сохранить текущую схему | 'SAVE' |
FETCH_SVG |
Запрос SVG-экспорта текущей схемы | - |
SET_LANGUAGE |
Смена языка интерфейса редактора | - |
Editor → Parent (исходящие):
| Message Type | Описание | Payload |
|---|---|---|
SCHEMA_CHANGED |
Уведомляет о наличии несохранённых изменений в схеме | { hasChanges: boolean } |
SCHEMA_UPDATE_SUCCESS |
Метаданные схемы успешно сохранены | - |
SCHEMA_UPDATE_ERROR |
Не удалось сохранить метаданные схемы | - |
SEATMAP_UPDATE_SUCCESS |
Графика/формы площадки успешно сохранены | - |
SEATMAP_UPDATE_ERROR |
Не удалось сохранить графику/формы площадки | { payload: unknown } |
UPDATE_PRICE_ASSIGNMENTS_SUCCESS |
Назначения цен успешно сохранены | - |
UPDATE_PRICE_ASSIGNMENTS_ERROR |
Не удалось сохранить назначения цен | { payload: unknown } |
PRICES_CHANGED |
Есть несохранённые изменения назначений цен | { hasPriceChanges: boolean } |
PRICE_CREATED |
В списке цен события создана цена | { price: { id, name } } |
PRICE_UPDATED |
Цена в списке цен события переименована | { price: { id, name } } |
PRICE_DELETED |
Цена удалена из списка цен события | { priceId: number } |
PUBLISH_REQUEST |
Пользователь нажал кнопку публикации | { payload: ISchemaSettings } |
SVG_RESPONSE |
Ответ на запрос FETCH_SVG (успех или ошибка) | { success, svg?, error? } |
Parent → Editor (входящие сообщения)
Сообщения, отправляемые из родительского окна в iframe редактора.
SAVE_REQUEST
Запускает сохранение текущей схемы редактором. Эта операция сохраняет все изменения, внесённые в макет площадки, включая места, секции, ряды, ценовые зоны и фоновую графику.
Что сохраняется:
- Макет и структура площадки
- Позиции, подписи и свойства мест
- Определения и границы секций
- Конфигурации и нумерация рядов
- Назначения ценовых зон
- Фоновые SVG/изображения и их позиционирование
- Пользовательские формы и подписи
- Настройки клиентского viewbox
Формат:
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, '*');
Legacy-формат (устаревший):
iframe.contentWindow.postMessage('SAVE', '*');
Ответные сообщения:
Операция сохранения может вызвать несколько ответных сообщений в зависимости от того, что было сохранено:
SCHEMA_UPDATE_SUCCESS- Метаданные и настройки схемы успешно сохраненыSCHEMA_UPDATE_ERROR- Не удалось сохранить схемуSEATMAP_UPDATE_SUCCESS- Графика/формы площадки успешно сохраненыSEATMAP_UPDATE_ERROR- Не удалось сохранить графику/формы площадкиUPDATE_PRICE_ASSIGNMENTS_SUCCESS- Назначения цен успешно сохраненыUPDATE_PRICE_ASSIGNMENTS_ERROR- Не удалось сохранить назначения цен
Базовый пример сохранения:
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');
}
});
Полный пример обработчика сохранения:
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');
}
}
Сохранение с индикатором загрузки:
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);
}
Реализация автосохранения:
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
Запрашивает SVG-экспорт текущей схемы. Схема должна быть сохранена перед экспортом.
Формат:
iframe.contentWindow.postMessage({ type: 'FETCH_SVG' }, '*');
Ответное сообщение:
SVG_RESPONSE- Содержит статус успеха, данные SVG или сообщение об ошибке
Пример:
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
Меняет язык интерфейса редактора во время работы. Позволяет синхронизировать язык встроенного редактора с языком вашего приложения без перезагрузки iframe.
Формат:
iframe.contentWindow.postMessage({ type: 'SET_LANGUAGE', language: 'pl' }, '*');
Поддерживаемые коды языков: en, ru, de, es, fr, it, zh, pl. Неподдерживаемые или некорректные коды игнорируются, текущий язык сохраняется.
Чтобы задать язык при первой загрузке редактора (до действий пользователя), добавьте параметр запроса language к URL iframe:
<iframe src="https://editor.seatmap.pro/app?language=pl" width="100%" height="800px"></iframe>
Пример:
const iframe = document.getElementById('seatmap-editor');
// Переключить редактор на польский
iframe.contentWindow.postMessage({ type: 'SET_LANGUAGE', language: 'pl' }, '*');
Editor → Parent (исходящие сообщения)
Сообщения, отправляемые из iframe редактора в родительское окно.
SCHEMA_CHANGED
Уведомляет родителя о наличии в схеме несохранённых изменений.
Payload:
{
type: 'SCHEMA_CHANGED',
hasChanges: boolean
}
Пример:
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
Отправляется после успешного завершения операции сохранения схемы.
Payload:
{
type: 'SCHEMA_UPDATE_SUCCESS';
}
SCHEMA_UPDATE_ERROR
Отправляется при сбое операции сохранения схемы.
Payload:
{
type: 'SCHEMA_UPDATE_ERROR';
}
SEATMAP_UPDATE_SUCCESS
Отправляется после успешного сохранения графики/форм площадки.
Payload:
{
type: 'SEATMAP_UPDATE_SUCCESS';
}
SEATMAP_UPDATE_ERROR
Отправляется при сбое сохранения графики/форм площадки.
Payload:
{
type: 'SEATMAP_UPDATE_ERROR';
}
SVG_RESPONSE
Ответ на запрос FETCH_SVG, содержащий экспортированный SVG или информацию об ошибке.
Payload при успехе:
{
type: 'SVG_RESPONSE',
success: true,
svg: string // SVG content as string
}
Payload при ошибке:
{
type: 'SVG_RESPONSE',
success: false,
error: string // Error message
}
Типичные сообщения об ошибках:
"Schema must be saved before export"- Схема ещё не была сохранена- Локализованные сообщения об ошибках экспорта из API
UPDATE_PRICE_ASSIGNMENTS_SUCCESS
Отправляется после успешного завершения обновления назначений цен.
Payload:
{
type: 'UPDATE_PRICE_ASSIGNMENTS_SUCCESS';
}
UPDATE_PRICE_ASSIGNMENTS_ERROR
Отправляется при сбое обновления назначений цен.
Payload:
{
type: 'UPDATE_PRICE_ASSIGNMENTS_ERROR',
payload: unknown // Error details
}
PRICES_CHANGED
Уведомляет родительское окно о наличии несохранённых изменений в назначениях цен — аналог SCHEMA_CHANGED для ценообразования. Отправляется с hasPriceChanges: true, когда пользователь назначает или снимает цены с мест или секций, и с hasPriceChanges: false после сохранения назначений.
Payload:
{
type: 'PRICES_CHANGED',
hasPriceChanges: boolean
}
PRICE_CREATED
Отправляется сразу после создания новой цены в списке цен события.
В отличие от назначений цен, сам список цен принадлежит событию и сохраняется немедленно: создание, переименование или удаление цены сразу вызывает бэкенд и не ждёт действия Save для схемы. Сообщения PRICE_CREATED, PRICE_UPDATED и PRICE_DELETED позволяют родительскому приложению синхронизировать собственный каталог цен с этими немедленными изменениями.
Payload:
{
type: 'PRICE_CREATED',
price: {
id: number,
name: string
}
}
Пример:
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
Отправляется сразу после переименования существующей цены в списке цен события.
Payload:
{
type: 'PRICE_UPDATED',
price: {
id: number,
name: string
}
}
PRICE_DELETED
Отправляется сразу после удаления цены из списка цен события.
Payload:
{
type: 'PRICE_DELETED',
priceId: number
}
PUBLISH_REQUEST
Отправляется, когда пользователь нажимает кнопку «Publish» в редакторе. Это означает, что схема публикуется из режима черновика в рабочий режим.
Payload:
{
type: 'PUBLISH_REQUEST',
payload: ISchemaSettings // Schema settings with draft: false
}
Пример:
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.)
}
});
Важные замечания:
- Это сообщение отправляется, когда пользователь инициирует публикацию, но до завершения серверной операции
- Схема всё равно будет сохранена через обычный поток сохранения, вызывая
SCHEMA_UPDATE_SUCCESSилиSCHEMA_UPDATE_ERROR - Родитель должен дождаться
SCHEMA_UPDATE_SUCCESS, чтобы подтвердить успешное завершение операции публикации
Пример процесса публикации:
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;
}
});
Полный пример интеграции
<!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>
Обработка ошибок
Типичные сценарии
Схема не сохранена
При запросе SVG-экспорта без предварительного сохранения:
// Response:
{
type: 'SVG_RESPONSE',
success: false,
error: 'Schema must be saved before export'
}
Решение: Сначала запустите сохранение, затем запросите 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);
}
Полный процесс «сохранить, затем экспортировать»:
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
Сетевые или серверные ошибки во время экспорта:
{
type: 'SVG_RESPONSE',
success: false,
error: 'Export failed' // Localized error message
}
Решение: Покажите понятное пользователю сообщение об ошибке и предложите возможность повтора.
Соображения по размеру сообщений
SVG-экспорты могут быть большими, особенно для сложных площадок:
- Небольшие площадки: 10KB - 100KB
- Средние площадки: 100KB - 1MB
- Крупные площадки: 1MB - 10MB+
API postMessage поддерживает сообщения размером до ~64MB в современных браузерах, но очень большие полезные нагрузки могут вызвать проблемы с производительностью. Для площадок с чрезвычайно сложной графикой рассмотрите следующие варианты:
- Использование прямого эндпоинта API (
/api/export/${schemaId}/) вместо этого - Скачивание через серверную обработку
- Внедрение индикаторов прогресса для пользователя
Тестирование
Тестовые файлы
В репозитории есть тестовые HTML-файлы для разработки и отладки:
-
public/sso_test.html- Полнофункциональная тестовая страница с:- Процессом SSO-аутентификации
- Встраиванием iframe в модальном окне
- Кнопкой запроса сохранения
- Панелью логирования сообщений
- Отображением сообщений в реальном времени
-
public/iframe_modal_test.html- Простой тест модального окна с:- Базовым встраиванием iframe
- Логированием событий сообщений
- Отслеживанием изменений схемы
Локальное тестирование
# Start development server
yarn start
# Open test page
open http://localhost:3000/sso_test.html
Соображения безопасности
Проверка origin
Всегда проверяйте origin получаемых сообщений в продакшене:
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
При отправке сообщений в iframe указывайте целевой origin:
// Instead of '*', use specific origin
iframe.contentWindow.postMessage({ type: 'SAVE_REQUEST' }, 'https://editor.seatmap.pro');
Аутентификация
Редактор требует JWT-аутентификации. Передавайте токены через параметры URL:
https://editor.seatmap.pro/app?token=JWT_TOKEN&refreshToken=REFRESH_TOKEN
Убедитесь, что токены передаются безопасно и имеют подходящее время истечения.
Устранение неполадок
Сообщения не доходят
-
Проверьте, что iframe загружен:
iframe.addEventListener('load', () => { console.log('Iframe loaded, ready for messages'); }); -
Проверьте доступ к contentWindow:
if (!iframe.contentWindow) { console.error('Cannot access iframe contentWindow'); } -
Проверьте ограничения cross-origin:
- Убедитесь, что заданы корректные заголовки CORS
- Используйте консоль браузера для проверки ошибок безопасности
Сохранение не работает
Типичные причины:
-
Проблемы аутентификации
- Токен истёк или недействителен
- У пользователя нет прав на запись
- Сессия истекла
-
Ошибки валидации
- Данные схемы неполные или недействительные
- Отсутствуют обязательные поля
- Ошибки формата данных
-
Сетевые проблемы
- Эндпоинт API недоступен
- Ограничения CORS
- Файрвол блокирует запросы
-
Проблемы состояния
- Нет изменений для сохранения
- Редактор не полностью загружен
- Параллельные операции сохранения
Шаги отладки:
// 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
Чек-лист операции сохранения:
- iframe редактора полностью загружен
- Пользователь аутентифицирован (токен присутствует)
- У пользователя есть права на редактирование площадки
- В схеме есть несохранённые изменения (отслеживайте
SCHEMA_CHANGED) - Никакая другая операция сохранения не выполняется
- Сетевое соединение стабильно
Сбой экспорта SVG
Типичные причины:
- Схема не сохранена (самая частая)
- Проблемы с сетевым соединением
- Серверные ошибки экспорта
- Недействительные данные схемы
Диаграмма потока сообщений
Поток операции сохранения
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
Поток операции публикации
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
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)
Типичные сценарии работы
Комплексный обработчик сообщений
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?';
}
});
Сохранение и закрытие модального окна
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();
}
}
Периодическое автосохранение с отображением статуса
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();
});
Процесс «сохранить — экспортировать — скачать — закрыть»
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);
}
Пакетные операции с отслеживанием прогресса
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
Определения типов для полезных нагрузок сообщений:
// 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;
}
}
});
Рекомендации
1. Всегда проверяйте origin сообщений
window.addEventListener('message', (event) => {
const allowedOrigins = ['https://editor.seatmap.pro', 'https://editor.seatmap.dev'];
if (!allowedOrigins.includes(event.origin)) {
return;
}
handleMessage(event.data);
});
2. Обрабатывайте все сообщения, связанные с сохранением
Полная операция сохранения может вызвать до 3 разных сообщений об успехе:
let saveResults = { schema: false, seatmap: false, prices: false };
function checkAllSaved() {
return saveResults.schema && saveResults.seatmap && saveResults.prices;
}
3. Реализуйте таймауты для операций
const timeout = setTimeout(() => {
console.error('Save operation timed out');
resetSaveState();
}, 30000); // 30 second timeout
// Clear timeout when operation completes
4. Отслеживайте состояние операций
const operationState = {
saveInProgress: false,
publishInProgress: false,
exportInProgress: false,
};
// Prevent duplicate operations
if (operationState.saveInProgress) {
console.warn('Save already in progress');
return;
}
5. Обеспечивайте обратную связь для пользователя
// Show loading indicators
// Display success/error notifications
// Update UI state based on messages
// Log operations for debugging
6. Обрабатывайте несохранённые изменения
window.addEventListener('beforeunload', (event) => {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = '';
}
});
7. Дождитесь сохранения схемы перед экспортом
async function saveAndExport() {
// Save first
await save();
// Then export
exportSvg();
}
Дополнительные ресурсы
- MDN: Window.postMessage()
- Документация Seatmap Editor API
- Реализация:
src/utils/parentMessaging.ts - Примеры интеграции:
public/sso_test.html,public/iframe_modal_test.html
Поддержка
По вопросам или проблемам с интеграцией iframe:
- Изучите существующие тестовые файлы с рабочими примерами
- Проверьте консоль браузера на наличие сообщений об ошибках
- Свяжитесь со службой поддержки, указав конкретные детали ошибки и информацию о браузере