mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 02:01:22 +01:00
Add external API integration for wiki/web UI synchronization
This commit adds a comprehensive external API system that allows external tools (wikis, web UIs, etc.) to control and synchronize with Fantasy Map Generator. New Features: - External API Bridge (modules/external-api.js) - Event-driven architecture with EventEmitter - Map lifecycle control (create, load, save) - Data access methods (rivers, cultures, states, burgs) - Data mutation methods with auto-redraw - Export support (SVG, PNG, JSON) - Change detection with automatic event emission - PostMessage Communication Layer - Auto-enables when FMG is embedded in iframe - Bidirectional message passing - Request/response pattern with promise support - Automatic event forwarding to parent window - REST API Server (api-server/) - Express.js server with full CRUD operations - WebSocket support via Socket.IO for real-time updates - File upload support for map and CSV import - In-memory storage (can be replaced with database) - CORS enabled for cross-origin requests - Comprehensive endpoints for all map data - Client Library (api-server/client.js) - Simple JavaScript client for REST API - Promise-based async methods - Works in browser and Node.js - Demo Pages (demos/) - PostMessage integration demo with full UI - REST API demo with interactive testing - WebSocket demo for real-time events - Documentation - Comprehensive integration guide (EXTERNAL_API_INTEGRATION.md) - API reference with TypeScript interfaces - Multiple integration examples - Troubleshooting guide Integration Methods: 1. PostMessage Bridge - For iframe embedding 2. REST API - For server-side integration 3. Direct JavaScript API - For same-origin apps Use Cases: - Wiki pages that need to display and control maps - Web UIs that want to edit map data - External tools that need to sync with FMG - Real-time collaborative map editing - Batch operations and automation Technical Details: - Zero dependencies for external-api.js (pure JS) - Auto-initializes on DOMContentLoaded - Throttled change detection (500ms debounce) - Deep cloning for data access (prevents mutations) - Error handling throughout - Version tagged (v1.0.0) Updated Files: - index.html: Added script tag to load external-api module All APIs are backward compatible and don't affect existing functionality.
This commit is contained in:
parent
dede314c94
commit
20458e39e2
10 changed files with 4699 additions and 0 deletions
770
modules/external-api.js
Normal file
770
modules/external-api.js
Normal file
|
|
@ -0,0 +1,770 @@
|
|||
/**
|
||||
* External API Bridge for Fantasy Map Generator
|
||||
* Provides a clean interface for external tools (wikis, web UIs) to interact with FMG
|
||||
* @module external-api
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// EVENT EMITTER SYSTEM
|
||||
// ============================================================================
|
||||
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(callback);
|
||||
return () => this.off(event, callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event].forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in event handler for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
once(event, callback) {
|
||||
const wrapper = (data) => {
|
||||
callback(data);
|
||||
this.off(event, wrapper);
|
||||
};
|
||||
this.on(event, wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
// ============================================================================
|
||||
// STATE MANAGEMENT & CHANGE DETECTION
|
||||
// ============================================================================
|
||||
|
||||
let isInitialized = false;
|
||||
let lastState = null;
|
||||
let changeDetectionEnabled = true;
|
||||
|
||||
function initializeChangeDetection() {
|
||||
if (isInitialized) return;
|
||||
|
||||
// Observe SVG changes for map updates
|
||||
const mapElement = document.getElementById('map');
|
||||
if (mapElement) {
|
||||
const observer = new MutationObserver(debounce(() => {
|
||||
if (changeDetectionEnabled) {
|
||||
eventEmitter.emit('map:changed', getMapState());
|
||||
}
|
||||
}, 500));
|
||||
|
||||
observer.observe(mapElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['d', 'transform', 'points']
|
||||
});
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CORE API METHODS
|
||||
// ============================================================================
|
||||
|
||||
const API = {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// MAP LIFECYCLE
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new map with optional parameters
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {Promise<Object>} Map state
|
||||
*/
|
||||
async createMap(options = {}) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
if (options.seed) {
|
||||
seed = options.seed;
|
||||
}
|
||||
|
||||
// Use existing generate function
|
||||
await generate(options);
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('map:created', getMapState());
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: getMapState()
|
||||
};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load map from data
|
||||
* @param {String|Blob|File} mapData - Map data to load
|
||||
* @returns {Promise<Object>} Load result
|
||||
*/
|
||||
async loadMap(mapData) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
let file;
|
||||
if (typeof mapData === 'string') {
|
||||
// Convert string data to Blob
|
||||
file = new Blob([mapData], {type: 'text/plain'});
|
||||
} else {
|
||||
file = mapData;
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
uploadMap(file, (error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('map:loaded', getMapState());
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: getMapState()
|
||||
};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save current map
|
||||
* @param {String} format - 'data' or 'blob'
|
||||
* @returns {Promise<Object>} Saved map data
|
||||
*/
|
||||
async saveMap(format = 'data') {
|
||||
try {
|
||||
const mapData = await prepareMapData();
|
||||
|
||||
if (format === 'blob') {
|
||||
return {
|
||||
success: true,
|
||||
data: new Blob([mapData], {type: 'text/plain'}),
|
||||
filename: getFileName() + '.map'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: mapData,
|
||||
filename: getFileName() + '.map'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DATA ACCESS
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get complete map state
|
||||
* @returns {Object} Current map state
|
||||
*/
|
||||
getMapState() {
|
||||
return getMapState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get specific data structure
|
||||
* @param {String} key - Data key (rivers, cultures, states, burgs, etc.)
|
||||
* @returns {*} Requested data
|
||||
*/
|
||||
getData(key) {
|
||||
if (!pack || !pack[key]) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(pack[key]));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get rivers data
|
||||
* @returns {Array} Rivers array
|
||||
*/
|
||||
getRivers() {
|
||||
return pack.rivers ? JSON.parse(JSON.stringify(pack.rivers)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cultures data
|
||||
* @returns {Array} Cultures array
|
||||
*/
|
||||
getCultures() {
|
||||
return pack.cultures ? JSON.parse(JSON.stringify(pack.cultures)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get states data
|
||||
* @returns {Array} States array
|
||||
*/
|
||||
getStates() {
|
||||
return pack.states ? JSON.parse(JSON.stringify(pack.states)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get burgs (cities/towns) data
|
||||
* @returns {Array} Burgs array
|
||||
*/
|
||||
getBurgs() {
|
||||
return pack.burgs ? JSON.parse(JSON.stringify(pack.burgs)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get religions data
|
||||
* @returns {Array} Religions array
|
||||
*/
|
||||
getReligions() {
|
||||
return pack.religions ? JSON.parse(JSON.stringify(pack.religions)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get markers data
|
||||
* @returns {Array} Markers array
|
||||
*/
|
||||
getMarkers() {
|
||||
return pack.markers ? JSON.parse(JSON.stringify(pack.markers)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get grid data
|
||||
* @returns {Object} Grid object with cells data
|
||||
*/
|
||||
getGrid() {
|
||||
if (!grid) return null;
|
||||
|
||||
return {
|
||||
spacing: grid.spacing,
|
||||
cellsX: grid.cellsX,
|
||||
cellsY: grid.cellsY,
|
||||
features: grid.features,
|
||||
boundary: grid.boundary
|
||||
};
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update rivers data
|
||||
* @param {Array} rivers - New rivers array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateRivers(rivers) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.rivers = rivers;
|
||||
|
||||
// Redraw rivers
|
||||
if (window.Rivers && Rivers.specify) {
|
||||
Rivers.specify();
|
||||
}
|
||||
if (typeof drawRivers === 'function') {
|
||||
drawRivers();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('rivers:updated', pack.rivers);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update cultures data
|
||||
* @param {Array} cultures - New cultures array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateCultures(cultures) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.cultures = cultures;
|
||||
|
||||
// Redraw cultures
|
||||
if (typeof drawCultures === 'function') {
|
||||
drawCultures();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('cultures:updated', pack.cultures);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update states data
|
||||
* @param {Array} states - New states array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateStates(states) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.states = states;
|
||||
|
||||
// Redraw states
|
||||
if (typeof drawStates === 'function') {
|
||||
drawStates();
|
||||
}
|
||||
if (typeof drawBorders === 'function') {
|
||||
drawBorders();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('states:updated', pack.states);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update burgs (cities/towns) data
|
||||
* @param {Array} burgs - New burgs array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateBurgs(burgs) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.burgs = burgs;
|
||||
|
||||
// Redraw burgs
|
||||
if (typeof drawBurgs === 'function') {
|
||||
drawBurgs();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('burgs:updated', pack.burgs);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new burg (city/town)
|
||||
* @param {Object} burgData - Burg properties
|
||||
* @returns {Object} Result with new burg ID
|
||||
*/
|
||||
addBurg(burgData) {
|
||||
try {
|
||||
const newId = pack.burgs.length;
|
||||
const burg = {
|
||||
i: newId,
|
||||
cell: burgData.cell || 0,
|
||||
x: burgData.x || 0,
|
||||
y: burgData.y || 0,
|
||||
name: burgData.name || Names.getCulture(burgData.culture || 0),
|
||||
population: burgData.population || 1,
|
||||
type: burgData.type || 'town',
|
||||
...burgData
|
||||
};
|
||||
|
||||
pack.burgs.push(burg);
|
||||
|
||||
if (typeof drawBurgs === 'function') {
|
||||
drawBurgs();
|
||||
}
|
||||
|
||||
eventEmitter.emit('burg:added', burg);
|
||||
|
||||
return {success: true, id: newId, burg};
|
||||
} catch (error) {
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// EXPORT
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Export map as SVG
|
||||
* @returns {String} SVG string
|
||||
*/
|
||||
exportSVG() {
|
||||
const svgElement = document.getElementById('map');
|
||||
if (!svgElement) return null;
|
||||
return svgElement.outerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* Export map as PNG
|
||||
* @param {Number} width - Image width
|
||||
* @param {Number} height - Image height
|
||||
* @returns {Promise<Blob>} PNG blob
|
||||
*/
|
||||
async exportPNG(width = 2048, height = 2048) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const svgElement = document.getElementById('map');
|
||||
const svgString = new XMLSerializer().serializeToString(svgElement);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const img = new Image();
|
||||
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
img.onload = function() {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
URL.revokeObjectURL(url);
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Export specific data as JSON
|
||||
* @param {String} key - Data key
|
||||
* @returns {String} JSON string
|
||||
*/
|
||||
exportJSON(key) {
|
||||
const data = key ? API.getData(key) : getMapState();
|
||||
return JSON.stringify(data, null, 2);
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// EVENTS
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Subscribe to events
|
||||
* @param {String} event - Event name
|
||||
* @param {Function} callback - Callback function
|
||||
* @returns {Function} Unsubscribe function
|
||||
*/
|
||||
on(event, callback) {
|
||||
return eventEmitter.on(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unsubscribe from events
|
||||
* @param {String} event - Event name
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
off(event, callback) {
|
||||
eventEmitter.off(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribe to event once
|
||||
* @param {String} event - Event name
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
once(event, callback) {
|
||||
eventEmitter.once(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit custom event
|
||||
* @param {String} event - Event name
|
||||
* @param {*} data - Event data
|
||||
*/
|
||||
emit(event, data) {
|
||||
eventEmitter.emit(event, data);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function getMapState() {
|
||||
return {
|
||||
seed: typeof seed !== 'undefined' ? seed : null,
|
||||
mapId: typeof mapId !== 'undefined' ? mapId : null,
|
||||
timestamp: Date.now(),
|
||||
pack: pack ? {
|
||||
cultures: pack.cultures || [],
|
||||
states: pack.states || [],
|
||||
burgs: pack.burgs || [],
|
||||
rivers: pack.rivers || [],
|
||||
religions: pack.religions || [],
|
||||
provinces: pack.provinces || [],
|
||||
markers: pack.markers || []
|
||||
} : null,
|
||||
grid: grid ? {
|
||||
spacing: grid.spacing,
|
||||
cellsX: grid.cellsX,
|
||||
cellsY: grid.cellsY,
|
||||
features: grid.features
|
||||
} : null,
|
||||
options: typeof options !== 'undefined' ? options : null
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POSTMESSAGE BRIDGE
|
||||
// ============================================================================
|
||||
|
||||
const PostMessageBridge = {
|
||||
enabled: false,
|
||||
targetOrigin: '*',
|
||||
|
||||
/**
|
||||
* Enable PostMessage communication
|
||||
* @param {String} origin - Target origin for postMessage (default: '*')
|
||||
*/
|
||||
enable(origin = '*') {
|
||||
if (this.enabled) return;
|
||||
|
||||
this.targetOrigin = origin;
|
||||
this.enabled = true;
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', this.handleMessage.bind(this));
|
||||
|
||||
// Forward all events to parent
|
||||
this.setupEventForwarding();
|
||||
|
||||
console.log('[FMG API] PostMessage bridge enabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable PostMessage communication
|
||||
*/
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
window.removeEventListener('message', this.handleMessage.bind(this));
|
||||
console.log('[FMG API] PostMessage bridge disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle incoming messages
|
||||
*/
|
||||
async handleMessage(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const {type, payload, requestId} = event.data;
|
||||
if (!type) return;
|
||||
|
||||
console.log('[FMG API] Received message:', type, payload);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch(type) {
|
||||
// Map lifecycle
|
||||
case 'CREATE_MAP':
|
||||
result = await API.createMap(payload);
|
||||
break;
|
||||
case 'LOAD_MAP':
|
||||
result = await API.loadMap(payload);
|
||||
break;
|
||||
case 'SAVE_MAP':
|
||||
result = await API.saveMap(payload?.format);
|
||||
break;
|
||||
|
||||
// Data access
|
||||
case 'GET_STATE':
|
||||
result = {success: true, data: API.getMapState()};
|
||||
break;
|
||||
case 'GET_DATA':
|
||||
result = {success: true, data: API.getData(payload?.key)};
|
||||
break;
|
||||
case 'GET_RIVERS':
|
||||
result = {success: true, data: API.getRivers()};
|
||||
break;
|
||||
case 'GET_CULTURES':
|
||||
result = {success: true, data: API.getCultures()};
|
||||
break;
|
||||
case 'GET_STATES':
|
||||
result = {success: true, data: API.getStates()};
|
||||
break;
|
||||
case 'GET_BURGS':
|
||||
result = {success: true, data: API.getBurgs()};
|
||||
break;
|
||||
|
||||
// Mutations
|
||||
case 'UPDATE_RIVERS':
|
||||
result = API.updateRivers(payload);
|
||||
break;
|
||||
case 'UPDATE_CULTURES':
|
||||
result = API.updateCultures(payload);
|
||||
break;
|
||||
case 'UPDATE_STATES':
|
||||
result = API.updateStates(payload);
|
||||
break;
|
||||
case 'UPDATE_BURGS':
|
||||
result = API.updateBurgs(payload);
|
||||
break;
|
||||
case 'ADD_BURG':
|
||||
result = API.addBurg(payload);
|
||||
break;
|
||||
|
||||
// Export
|
||||
case 'EXPORT_SVG':
|
||||
result = {success: true, data: API.exportSVG()};
|
||||
break;
|
||||
case 'EXPORT_PNG':
|
||||
const blob = await API.exportPNG(payload?.width, payload?.height);
|
||||
const reader = new FileReader();
|
||||
result = await new Promise((resolve) => {
|
||||
reader.onload = () => resolve({success: true, data: reader.result});
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
break;
|
||||
case 'EXPORT_JSON':
|
||||
result = {success: true, data: API.exportJSON(payload?.key)};
|
||||
break;
|
||||
|
||||
default:
|
||||
result = {success: false, error: `Unknown message type: ${type}`};
|
||||
}
|
||||
|
||||
// Send response
|
||||
this.sendMessage('RESPONSE', result, requestId);
|
||||
} catch (error) {
|
||||
this.sendMessage('ERROR', {
|
||||
success: false,
|
||||
error: error.message
|
||||
}, requestId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send message to parent window
|
||||
*/
|
||||
sendMessage(type, payload, requestId = null) {
|
||||
if (!this.enabled) return;
|
||||
if (window.parent === window) return; // Not in iframe
|
||||
|
||||
window.parent.postMessage({
|
||||
type,
|
||||
payload,
|
||||
requestId,
|
||||
timestamp: Date.now()
|
||||
}, this.targetOrigin);
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward API events to parent window
|
||||
*/
|
||||
setupEventForwarding() {
|
||||
const events = [
|
||||
'map:created',
|
||||
'map:loaded',
|
||||
'map:changed',
|
||||
'rivers:updated',
|
||||
'cultures:updated',
|
||||
'states:updated',
|
||||
'burgs:updated',
|
||||
'burg:added'
|
||||
];
|
||||
|
||||
events.forEach(event => {
|
||||
API.on(event, (data) => {
|
||||
this.sendMessage('EVENT', {event, data});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
// Initialize on DOMContentLoaded
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeChangeDetection();
|
||||
});
|
||||
} else {
|
||||
initializeChangeDetection();
|
||||
}
|
||||
|
||||
// Auto-enable PostMessage if in iframe
|
||||
if (window.self !== window.top) {
|
||||
setTimeout(() => PostMessageBridge.enable(), 1000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORT API
|
||||
// ============================================================================
|
||||
|
||||
window.FMG_API = API;
|
||||
window.FMG_PostMessageBridge = PostMessageBridge;
|
||||
|
||||
console.log('[FMG API] External API initialized. Access via window.FMG_API');
|
||||
console.log('[FMG API] Available methods:', Object.keys(API));
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue