/**
* AngularJS Service for handling CMS builder preview mode.
*
* When the game is loaded with ?preview=true inside an iframe, this service
* replaces the normal API data-loading flow with the following:
* 1. Game sends CHILD_READY to the parent (tga-admin)
* 2. Parent responds with GAME_DATA containing the full game object
* 3. On each builder edit, parent re-sends GAME_DATA with updated data
*
* @module previewService
* @requires $window
* @requires $rootScope
* @requires $log
* @requires URLParamsService
*/
angular.module('tgaApp').service('previewService', [
'$window',
'$rootScope',
'$log',
'$timeout',
'URLParamsService',
function previewService($window, $rootScope, $log, $timeout, URLParamsService) {
const READY_TIMEOUT_MS = 4000;
let inIframe;
try {
inIframe = $window.self !== $window.top;
} catch (e) {
inIframe = true;
}
const previewParam = URLParamsService.getParam('preview');
const active = previewParam === 'true' && inIframe;
let messageHandler = null;
/**
* Whether the app is running in preview mode.
* True only when ?preview=true AND loaded inside an iframe.
*
* @returns {boolean}
*/
this.isPreview = () => active;
/**
* Post CHILD_READY to the parent window so the CMS knows
* the iframe is ready to receive game data.
*/
this.sendReady = () => {
$window.parent.postMessage({ type: 'CHILD_READY' }, '*');
$log.debug('previewService: CHILD_READY sent');
};
/**
* Listen for GAME_DATA messages from the parent window.
* The first message triggers the initial load callback.
* Subsequent messages broadcast 'preview:update' on $rootScope.
*
* If no GAME_DATA arrives within READY_TIMEOUT_MS, the fallback
* callback is invoked so the game can fall back to normal API loading.
*
* @param {Function} initialCallback - called once with the first GAME_DATA message
* @param {Function} [fallbackCallback] - called if no response within timeout
*/
this.listen = (initialCallback, fallbackCallback) => {
if (!active) return;
let initialReceived = false;
const timeoutId = $timeout(() => {
if (!initialReceived) {
$log.warn('previewService: no GAME_DATA received within timeout, falling back to normal load');
this.destroy();
if (fallbackCallback) fallbackCallback();
}
}, READY_TIMEOUT_MS);
messageHandler = (event) => {
const message = event.data;
if (!message) return;
if (message.type === 'PREVIEW_ACTION') {
$rootScope.$applyAsync(() => {
$rootScope.$broadcast('preview:action', message.action);
});
return;
}
if (message.type !== 'GAME_DATA') return;
if (!initialReceived) {
initialReceived = true;
$window.clearTimeout(timeoutId);
$rootScope.$applyAsync(() => {
initialCallback(message);
});
} else {
$rootScope.$applyAsync(() => {
$rootScope.$broadcast('preview:update', message);
});
}
};
$window.addEventListener('message', messageHandler);
};
/**
* Post state back to the parent CMS so it can update its UI.
* @param {Object} state - arbitrary state object (e.g. { tossupPlaying: true })
*/
this.sendState = (state) => {
$window.parent.postMessage({ type: 'PREVIEW_STATE', state }, '*');
};
/**
* Remove the message event listener.
*/
this.destroy = () => {
if (messageHandler) {
$window.removeEventListener('message', messageHandler);
messageHandler = null;
}
};
},
]);