import type {EmbeddingMode, EmbedsConfig, HTMLRuntimeConfig} from '../types';
import type {IEmbeddedContentController} from './IEmbeddedContentController';
import type {IHTMLIFrameElementConfig} from '.';
import {nanoid} from 'nanoid';
import {Disposable} from '../utils';
import {HTML_RUNTIME_CONFIG_SYMBOL} from '../constants';
import {EmbeddedIFrameController} from './EmbeddedIFrameController';
import {ShadowRootController} from './ShadowRootController';
import {SrcDocIFrameController} from './SrcDocIFrameController';
const findAllSrcDocEmbeds = (scope: ParentNode) =>
scope.querySelectorAll('iframe[data-yfm-sandbox-mode=srcdoc]');
const findAllShadowContainers = (scope: ParentNode) =>
scope.querySelectorAll('div[data-yfm-sandbox-mode=shadow]');
const findAllIFrameEmbeds = (scope: ParentNode) =>
scope.querySelectorAll('iframe[data-yfm-sandbox-mode=isolated]');
const embedFinders: Record NodeListOf> = {
srcdoc: findAllSrcDocEmbeds,
shadow: findAllShadowContainers,
isolated: findAllIFrameEmbeds,
};
const modeToController: Record<
EmbeddingMode,
new (node: HTMLElement, config: IHTMLIFrameElementConfig) => IEmbeddedContentController
> = {
srcdoc: SrcDocIFrameController,
shadow: ShadowRootController,
isolated: EmbeddedIFrameController,
};
// Finds all iframes and creates controllers for each iframe
export class EmbeddedContentRootController extends Disposable {
private children: Map = new Map();
private config: EmbedsConfig;
private document: Document;
private runtimeConfig: HTMLRuntimeConfig;
constructor(
document: Document,
config: EmbedsConfig = {
classNames: [],
styles: {},
},
) {
super();
this.config = config;
this.document = document;
this.runtimeConfig = window[HTML_RUNTIME_CONFIG_SYMBOL] || {};
// initialize on DOM ready
this.document.addEventListener('DOMContentLoaded', () => this.initialize());
this.dispose.add(() => {
this.document.removeEventListener('DOMContentLoaded', () => this.initialize());
});
this.dispose.add(() => this.disposeChildren());
}
initialize = async (configOverrideForThisInitCycle?: EmbedsConfig) => {
const {disabledModes} = this.runtimeConfig;
// MAJOR: separate runtime controllers and chunks, so the consumer could
// import only one runtime mode: import('@diplodoc/html-extension/runtime/srcdoc');
const embeds = Object.keys(embedFinders).reduce((result, current) => {
const modeKey = current as EmbeddingMode;
if (!disabledModes?.includes(modeKey)) {
return result.concat(...embedFinders[modeKey](this.document));
}
return result;
}, []);
const dirtyEmbeds = embeds.filter(
(el) =>
typeof el.dataset.yfmEmbedId !== 'string' ||
!this.children.has(el.dataset.yfmEmbedId),
);
const instantiatedControllers = dirtyEmbeds.map((embed) => {
const embedId = nanoid();
const mode = embed.dataset.yfmSandboxMode as EmbeddingMode; // this cast is safe at this point
const ControllerCtor = modeToController[mode];
const instance = new ControllerCtor(
embed,
configOverrideForThisInitCycle ?? this.config,
);
embed.dataset.yfmEmbedId = embedId;
this.children.set(embedId, instance);
return instance;
});
return Promise.all(instantiatedControllers.map((ctrller) => ctrller.initialize()));
};
get blocks(): IEmbeddedContentController[] {
return Array.from(this.children.values());
}
/**
* Set the config object that will be passed to child embeds during an initialization cycle.
* Please note that changes made via a call to this method would be only reflected during next initialization cycles.
* @param config
* @returns {void}
*/
setConfig(config: EmbedsConfig) {
this.config = config;
}
forEach(callback: (controller: IEmbeddedContentController) => void) {
return this.children.forEach((controller) => callback(controller));
}
disposeChildren = () => {
this.children.forEach((controller) => controller.dispose());
this.children.clear();
};
}