UNPKG

@3dsource/metabox-front-api

Version:

API for Metabox BASIC configurator

864 lines (832 loc) 29.5 kB
import { fromEvent } from 'rxjs'; /** * Base class for commands sent to the Metabox API. */ class CommandBase { } /** * Wrapper helper function to use it with rxJs the same as `fromEvent` to listen to events from the Communicator. * @param {Communicator} target * @param eventName */ function fromCommunicatorEvent(target, eventName) { return fromEvent(target, eventName); } const MetaboxBasicConfiguratorActions = { metaboxConfig: 'metaboxConfig', setEnvironment: 'setEnvironment', setEnvironmentMaterialById: 'setEnvironmentMaterialById', setProduct: 'setProduct', setProductMaterialById: 'setProductMaterialById', getPdf: 'getPdf', getCallToActionInformation: 'getCallToActionInformation', getScreenshot: 'getScreenshot', showEmbeddedMenu: 'showEmbeddedMenu', forceSetDeviceType: 'forceSetDeviceType', showOverlayInterface: 'showOverlayInterface', resetCamera: 'resetCamera', applyZoom: 'applyZoom', initShowcase: 'initShowcase', playShowcase: 'playShowcase', pauseShowcase: 'pauseShowcase', stopShowcase: 'stopShowcase', sendCommandToUnreal: 'sendCommandToUnreal', watchCallbacks: 'watchCallbacks', }; /** * Represents a command to send Metabox Config to Metabox Basic Configurator. * @remarks * Automatically sent when the Communicator instance is ready. */ class MetaboxConfig extends CommandBase { /** * Creates an instance of MetaboxConfig. * @param {string} appId - The unique identifier for the Communicator Instance * @param {MetaboxCommandConfig} config - optional initial config: standalone - if true - disable metabox custom template and all logic */ constructor(appId, config) { super(); this.data = { action: MetaboxBasicConfiguratorActions.metaboxConfig, payload: { appId, config }, }; } } /** * Represents a command to get a PDF from Metabox Basic Configurator. * @remarks * This class sends a message to the Metabox API to generate a PDF based on the current configuration. * This action does not require any parameters. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { GetPdf, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new GetPdf()); * }; */ class GetPdf extends CommandBase { /** * Creates an instance of GetPdf. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.getPdf }; } } /** * Represents a command to get a Call To Action Information from Metabox Basic Configurator. * @remarks * This class sends a message to the Metabox API to generate Call To Action information based on the current configuration and sends it to the endpoint url from the cta information. * This action does not require any parameters. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { GetCallToActionInformation, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new GetCallToActionInformation()); * }; */ class GetCallToActionInformation extends CommandBase { /** * Creates an instance of GetCallToActionInformation. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.getCallToActionInformation, }; } } /** * Represents a command to send to unreal and reset camera to initial. * @remarks * This class sends a message to the Metabox API to reset the camera on a scene. * This action does not require any parameters. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { ResetCamera, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new ResetCamera()); * }; */ class ResetCamera extends CommandBase { /** * Creates an instance of ResetCamera. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.resetCamera }; } } /** * Represents a command to ApplyZoom and change zoom camera on a scene. * @remarks * This class sends a command to the Metabox API to change zoom on a scene. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { ApplyZoom, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new ApplyZoom(10)); * api.sendCommandToMetabox(new ApplyZoom(-10)); * }; */ class ApplyZoom extends CommandBase { /** * Creates an instance of ApplyZoom. * * @param {...number} zoom */ constructor(zoom) { super(); this.data = { action: MetaboxBasicConfiguratorActions.applyZoom, payload: { zoom }, }; } } /** * Represents a command to Init Showcase for a product a scene and start a sequence for the product. * @remarks * This class sends a command to the Metabox API to init showcase if the product has a sequence. * For check this needs to find a showcase property in the current product. * If this property exists, you can send init showcase command. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * ```typescript * import { InitShowcase, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new InitShowcase()); * }; */ class InitShowcase extends CommandBase { /** * Creates an instance of InitShowcase. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.initShowcase, }; } } /** * Represents a command to Play Showcase for a product when it already init, and you call pause, for example, before it. * @remarks * This class sends a command to the Metabox API to play showcase for a product if it is already initialized and pause. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { PlayShowcase, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new PlayShowcase()); * }; */ class PlayShowcase extends CommandBase { /** * Creates an instance of PlayShowcase. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.playShowcase }; } } /** * @internal * @hidden * Represents a command to send Unreal command. */ class UnrealCommand extends CommandBase { constructor(payload) { super(); this.data = { action: MetaboxBasicConfiguratorActions.sendCommandToUnreal, payload, }; } } /** * Represents a command to Pause Showcase for a product when it already init and play, and you call pause. * @remarks * This class sends a command to the Metabox API to pause showcase for a product if it is already initialized and play. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { PauseShowcase, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new PauseShowcase()); * }; */ class PauseShowcase extends CommandBase { /** * Creates an instance of PauseShowcase. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.pauseShowcase }; } } /** * Represents a command to Stop Showcase for a product when it already init, and you want to destroy it. * @remarks * This class sends a command to the Metabox API to Stop showcase for a product if it is already initialized. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { StopShowcase, Communicator } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new StopShowcase()); * }; */ class StopShowcase extends CommandBase { /** * Creates an instance of StopShowcase. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.stopShowcase }; } } /** * Represents a command to get a screenshot. * * **Important**: Invoking this command will buffer it until the viewport is ready. Only then will the request be processed. * * @example * import { GetScreenshot, Communicator, saveImage } from '@3dsource/metabox-front-api'; * window.env3DSource.apiReady = (api: Communicator) => { * // Listen for events from to the Metabox API * api.addEventListener('screenshot', (data) => { * // Process the get screenshot response * saveImage(data, 'Render.png'); * }); * api.sendCommandToMetabox(new GetScreenshot('image/png', { x: 1024, y: 1024 })); * }; */ class GetScreenshot extends CommandBase { /** * Constructs an instance of GetScreenshot. * @param {MimeType} mimeType - The output format. * @param {{ x: number; y: number }} [size] - Optional size in pixels. */ constructor(mimeType, size) { super(); this.data = { action: MetaboxBasicConfiguratorActions.getScreenshot, payload: { format: mimeType, size }, }; } } /** * Represents a command to set a product by its ID. * * @remarks * This action sends a message to the Metabox API to set a product using the provided product ID. * * @example * import { SetProduct, Communicator } from '@3dsource/metabox-front-api'; * * //...Assume that the integration is already implemented * window.env3DSource.apiReady = (api: Communicator) => { * api.setCommandToMetaBox(new SetProduct( * 'ffea6b5c-3a8a-4f56-9417-e605acb5cca3' * )); * }; */ class SetProduct extends CommandBase { /** * Creates an instance of SetProduct. * * @param {string} productId - The product ID. */ constructor(productId) { super(); this.data = { action: MetaboxBasicConfiguratorActions.setProduct, payload: { productId }, }; } } /** * Represents a command to set a material by its slot ID and Material ID. * * @example * import { SetProductMaterial, Communicator } from '@3dsource/metabox-front-api'; * * //...Assume that the integration is already implemented * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new SetProductMaterial( * 'carpaint', * 'dd829d6e-9200-47a7-8d5b-af5df89b7e91', * )); * }; */ class SetProductMaterial extends CommandBase { /** * Creates an instance of SetProductMaterial. * * @param {string} slotId - The slot ID. * @param {string} materialId - The material ID. */ constructor(slotId, materialId) { super(); this.data = { action: MetaboxBasicConfiguratorActions.setProductMaterialById, payload: { slotId, materialId }, }; } } /** * Represents a command to set the environment by its ID. * * @example * import { SetEnvironment, Communicator } from '@3dsource/metabox-front-api'; * * //...Assume that the integration is already implemented * window.env3DSource.apiReady = (api: Communicator) => { * // Change the environment to '55555555-1234-1234-1234-01234567890' * api.sendCommandToMetabox(new SetEnvironment('55555555-1234-1234-1234-01234567890')); * }; */ class SetEnvironment extends CommandBase { /** * Creates an instance of SetEnvironment. * * @param {string} environmentId - The environment ID. */ constructor(environmentId) { super(); this.data = { action: MetaboxBasicConfiguratorActions.setEnvironment, payload: { id: environmentId }, }; } } /** * Represents a command to set an environment material by its slot ID and Material ID. * * @example * import { SetEnvironmentMaterial, Communicator } from '@3dsource/metabox-front-api'; * * //...Assume that the integration is already implemented * window.env3DSource.apiReady = (api: Communicator) => { * api.sendCommandToMetabox(new SetEnvironmentMaterial( * 'carpaint', * 'dd829d6e-9200-47a7-8d5b-af5df89b7e91', * )); * }; */ class SetEnvironmentMaterial extends CommandBase { /** * Creates an instance of SetEnvironmentMaterial. * * @param {string} slotId - The slot ID. * @param {string} materialId - The material ID. */ constructor(slotId, materialId) { super(); this.data = { action: MetaboxBasicConfiguratorActions.setEnvironmentMaterialById, payload: { slotId, materialId }, }; } } /** * Represents a command to toggle the Embedded to the Metabox Menu. * * @example * import { ShowEmbeddedMenu, Communicator } from '@3dsource/metabox-front-api'; * * //...Assume that the integration is already implemented * window.env3DSource.apiReady = (api: Communicator) => { * api.setCommandToMetaBox(new ShowEmbeddedMenu(true)); * }; */ class ShowEmbeddedMenu extends CommandBase { /** * Creates an instance of ShowEmbeddedMenu. * * @param {boolean} visible - A flag indicating whether the embedded menu should be visible. */ constructor(visible) { super(); this.data = { action: MetaboxBasicConfiguratorActions.showEmbeddedMenu, payload: { visible }, }; } } /** * Represents a command to toggle the Unreal Overlay Interface Menu. * * @remarks * This action sends a message to the Metabox API to toggle the visibility of the Unreal overlay UI. * * @example * import { ShowOverlayInterface, Communicator } from '@3dsource/metabox-front-api'; * * //...Assume that the integration is already implemented * window.env3DSource.apiReady = (api: Communicator) => { * api.setCommandToMetaBox(new ShowOverlayInterface(true)); * }; */ class ShowOverlayInterface extends CommandBase { /** * Creates an instance of ShowOverlayInterface. * * @param {boolean} visible - A flag indicating whether the Unreal overlay UI should be visible. */ constructor(visible) { super(); this.data = { action: MetaboxBasicConfiguratorActions.showOverlayInterface, payload: { visible }, }; } } /** * @experimental * @internal * @hidden * @remarks * Represents a command to watch Unreal callbacks from Unreal engine. * Can be useful for custom solutions. */ class WatchCallbacks extends CommandBase { /** * Creates an instance of WatchCallbacks. * * @remarks * This constructor does not require any parameters. */ constructor() { super(); this.data = { action: MetaboxBasicConfiguratorActions.watchCallbacks }; } } /** * EventDispatcher is a class that manages event listeners and dispatches events to them. */ class EventDispatcher { constructor() { /** * Storage for callback listeners by message type. */ this.listeners = []; } // eslint-disable-next-line @typescript-eslint/no-unused-vars destroy(key) { this.listeners = []; } /** * Adds an event listener for receiving specific types of messages. * * @param messageType - The message type to listen for. * @param callback - The callback function to execute when a message is received. */ addEventListener(messageType, callback) { this.listeners.push({ messageType, callback }); return this; } /** * Dispatches an event to all listeners of a specific message type. * * @param messageType - The message type. * @param data - The data associated with the event. */ dispatchEvent(messageType, data) { this.listeners .filter((listener) => listener.messageType === messageType) .forEach((listener) => listener.callback(data)); return this; } /** * Removes an event listener for a specific type of message. * * @param messageType - The message type. * @param callback - The callback function to remove. */ removeEventListener(messageType, callback) { this.listeners = this.listeners.filter((listener) => !(listener.messageType === messageType && listener.callback === callback)); return this; } } const Metabox = 'metabox_v3'; const MetaboxHost = 'metaboxHost_v3'; const MetaboxData = 'metaboxData_v3'; const AppLoaded = 'appLoaded_v3'; const MetaboxDomain = 'metabox.3dsource.com'; const BasicRouteUrl = 'metabox-configurator/basic'; const VERSION = '3.0.5'; const communicatorMap = new Map(); /** * Handles messaging between the host page and embedded Metabox content. * @internal Use {@link Communicator.createInstance} or the {@link integrateMetabox} helper to instantiate. */ class Communicator extends EventDispatcher { /** * Constructs a Communicator, replacing any existing instance, and begins listening for messages. * @internal */ constructor(data, environment, config) { super(); /** * Bound handler for incoming postMessage events. */ this.binder = this.handleMessageReceived.bind(this, MetaboxData); const { appId = 'unknown', version = VERSION } = data ?? {}; const key = `${version}_${environment}_${appId}`; communicatorMap.get(key)?.instance.destroy(key); communicatorMap.set(key, { instance: this }); window.addEventListener('message', this.binder); this.sendCommandToMetabox(new MetaboxConfig(appId, { ...config, hostUrl: location.href, apiVersion: VERSION, })); } /** * Listens for Metabox to signal readiness, then initializes communicator. * @param apiReadyCallback - Called with the new Communicator once to the Metabox is loaded. * @param {MetaboxEnvironment} environment - The environment in which the Communicator is running. * @param {MetaboxCommandConfig} config - optional initial config: standalone - if true - disable metabox custom template and all logic */ static createInstance(apiReadyCallback, environment, config) { const startHandler = (event) => { const message = event.data; if (message?.envelope?.action === AppLoaded && message.host === Metabox) { apiReadyCallback(new Communicator(message.envelope.payload, environment, config)); window.removeEventListener('message', startHandler); } }; window.addEventListener('message', startHandler); } /** * Cleans up resources and stops listening for messages. */ destroy(key) { super.destroy(key); window.removeEventListener('message', this.binder); } static clearAll() { if (communicatorMap.size === 0) { return; } communicatorMap.forEach((value, key) => value.instance.destroy(key)); communicatorMap.clear(); } static getCommunicator(key) { return communicatorMap.get(key); } /** * Posts a command to the Metabox iframe. * @param command - An action command containing data to send. */ sendCommandToMetabox(command) { const { data } = command; const iframe = document.getElementById('embeddedContent'); const message = { host: MetaboxHost, payload: { ...data }, }; if (!iframe?.contentWindow) { console.warn('to the Metabox IFrame not found or not ready. Message sends to the same window'); window.postMessage({ ...message, target: 'metabox' }, '*'); return; } // Replace '*' with a specific origin in production for better security. iframe.contentWindow.postMessage({ ...message, target: 'child' }, '*'); } /** * Registers an event listener for messages dispatched by to the Metabox. * @override */ addEventListener(messageType, callback) { return super.addEventListener(messageType, callback); } /** * Dispatches a typed event to all registered listeners. * @override */ dispatchEvent(messageType, data) { return super.dispatchEvent(messageType, data); } /** * Removes a previously registered event listener. * @override */ removeEventListener(messageType, callback) { return super.removeEventListener(messageType, callback); } /** * Filters and dispatches incoming messages from to the Metabox. * @param {FromMetaBoxApiEvents | 'metaboxData' } messageType - Expected message type for filtering ('metaboxData'). * @param {MessageEvent} event - The postMessage event received on a window. */ handleMessageReceived(messageType, event) { // Optionally validate event.origin for security. if (messageType !== MetaboxData || event.data?.host !== Metabox) { return; } const data = event.data?.envelope; this.dispatchEvent(data?.eventType, data?.payload); } } const prepareIframeSrc = (configuratorId, config) => { const base = `https://${config?.domain || MetaboxDomain}/${BasicRouteUrl}/${configuratorId}`; const params = new URLSearchParams(); if (config?.introImage) { params.set('introImage', config.introImage); } if (config?.introVideo) { params.set('introVideo', config.introVideo); } if (config?.loadingImage) { params.set('loadingImage', config.loadingImage); } if (config?.state) { params.set('state', decodeURIComponent(config.state)); } const query = params.toString(); return query ? `${base}?${query}` : base; }; /** * Integrates the Metabox Basic Configurator into the page by injecting an iframe and * initializing a Communicator instance for host-to-iframe messaging. * * @remarks * - Builds a secure iframe URL using the provided configuratorId and config options. * - Ensures the resulting URL uses HTTPS and that the target container exists. * - Removes any previously embedded iframe with id "embeddedContent" before inserting a new one. * * @param {string} configuratorId - The Basic Configurator ID (not a full URL). It is appended to * `https://{domain}/metabox-configurator/basic/{configuratorId}` to form the iframe src. * * @param {string} [containerId='embed3DSource'] - The id of the container element where the iframe will be injected. * * @param {(api: Communicator) => void} apiReadyCallback - Called when the Communicator instance is created on the host side. * * @param {IntegrateMetaboxConfig} config - Optional configuration used to build the iframe URL and initialize the communicator. * Supported fields: * - standalone?: boolean — if true, disables Metabox custom template and related logic. * - introImage?: string — URL to an image shown on the intro screen (added as ?introImage=...). * - introVideo?: string — URL to a video shown on the intro screen (added as ?introVideo=...). * - loadingImage?: string — URL to an image displayed while loading (added as ?loadingImage=...). * - state?: string — Predefined state for configurator for initial loading (added as ?state=...). * - domain?: string — custom domain for testing (defaults to metabox.3dsource.com). HTTPS is enforced. * * @throws Error If configuratorId or containerId are empty strings. * @throws Error If the computed iframe URL is invalid or does not use HTTPS. * @throws Error If the container element with the provided id cannot be found. * * @example * import { integrateMetabox } from '@3dsource/metabox-front-api'; * * integrateMetabox( * 'configurator-id', * 'embed3DSource', * (api) => { * // Communicator is ready to use * }, * { * standalone: false, * introImage: 'https://example.com/intro.png', * loadingImage: 'https://example.com/loading.png', * }, * ); */ function integrateMetabox(configuratorId, containerId = 'embed3DSource', apiReadyCallback, config) { if (!configuratorId.trim()) { throw new Error('integrateMetabox: configuratorId must be a non-empty string'); } const iframeSrc = prepareIframeSrc(configuratorId, config); let parsedUrl; try { parsedUrl = new URL(iframeSrc); } catch { throw new Error('integrateMetabox: Provided iframeSrc is not a valid URL'); } if (parsedUrl.protocol !== 'https:') { throw new Error('integrateMetabox: iframeSrc must use HTTPS protocol'); } if (!containerId.trim()) { throw new Error('integrateMetabox: containerId must be a non-empty string'); } const container = document.getElementById(containerId); if (!container) { throw new Error(`Container element with id ${containerId} not found`); } const existingIframe = document.getElementById('embeddedContent'); if (existingIframe) { existingIframe.remove(); } Communicator.createInstance(apiReadyCallback, 'host', config); const style = document.createElement('style'); style.innerHTML = ` #${containerId} { width: 100%; height: 100%; overflow: hidden; position: relative; } `; document.head.appendChild(style); const iframe = document.createElement('iframe'); iframe.setAttribute('allow', 'autoplay; fullscreen; encrypted-media'); iframe.setAttribute('referrerPolicy', 'no-referrer-when-downgrade'); iframe.setAttribute('id', 'embeddedContent'); iframe.style.border = '0'; iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.overflow = 'hidden'; iframe.src = iframeSrc; container.appendChild(iframe); } /** * Saves an image by triggering a download. * * @remarks * This function creates an anchor element, sets its `href` attribute to the provided image URL, * and triggers a click event to initiate a download with the specified filename. * * @param {string} imageUrl - The URL of the image to save. * @param {string} filename - The name of the file to save. */ function saveImage(imageUrl, filename) { const a = document.createElement('a'); a.href = imageUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); } /** * Sets the URL parameters based on the provided state. * * @remarks * This function updates the current URL by replacing its search parameters with the key-value pairs from the provided state object. * * @param {Record<string, string>} state - An object containing key-value pairs to set as URL parameters. */ function setUrlParams(state) { const url = new URL(window.location.href); url.search = ''; Object.entries(state).forEach(([key, value]) => { url.searchParams.set(key, value); }); history.replaceState(null, '', url.toString()); } /** * Retrieves the URL parameters as an object. * * @remarks * This function parses the current URL's search parameters and returns them as a key-value object. * * @returns An object containing the URL parameters. */ function getUrlParams() { const urlParams = new URLSearchParams(window.location.search); const selections = {}; urlParams.forEach((value, key) => { selections[key] = value; }); return selections; } /** * Generated bundle index. Do not edit. */ export { AppLoaded, ApplyZoom, BasicRouteUrl, CommandBase, Communicator, EventDispatcher, GetCallToActionInformation, GetPdf, GetScreenshot, InitShowcase, Metabox, MetaboxBasicConfiguratorActions, MetaboxConfig, MetaboxData, MetaboxDomain, MetaboxHost, PauseShowcase, PlayShowcase, ResetCamera, SetEnvironment, SetEnvironmentMaterial, SetProduct, SetProductMaterial, ShowEmbeddedMenu, ShowOverlayInterface, StopShowcase, UnrealCommand, VERSION, WatchCallbacks, fromCommunicatorEvent, getUrlParams, integrateMetabox, prepareIframeSrc, saveImage, setUrlParams }; //# sourceMappingURL=3dsource-metabox-front-api.mjs.map