Skip to content

Commit

Permalink
Add inheritance to the IFrameController
Browse files Browse the repository at this point in the history
  • Loading branch information
Avol-V committed Feb 13, 2025
1 parent fa38f42 commit e4e6d99
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 74 deletions.
63 changes: 63 additions & 0 deletions src/iframe/BaseIFrameController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {Disposable, updateClassNames, updateStyles} from '../utils';

export abstract class BaseIFrameController extends Disposable {
protected readonly iframe: HTMLIFrameElement | Window;
protected readonly domContainer: HTMLElement;
private classNames: string[] = [];
private styles: Record<string, string> = {};

constructor(iframe: HTMLIFrameElement | Window) {
super();

this.iframe = iframe;
this.domContainer =
'contentWindow' in iframe
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
iframe.contentWindow!.document.body
: iframe.document.body;
}

setClassNames = (classNames: string[] | undefined = []) => {
updateClassNames(this.domContainer, classNames, this.classNames);

// update this._classNames to the new classNames
this.classNames = classNames;
};

setStyles = (styles: Record<string, string> | undefined = {}) => {
updateStyles(this.domContainer, styles, this.styles);

// update this._styles to the new styles
this.styles = styles;
};

replaceHTML = (htmlString: string) => {
const fragment = globalThis.document.createRange().createContextualFragment(htmlString);

this.domContainer.replaceChildren(fragment);

// TODO: should we reinitialize Observer?
};

setBaseTarget = (value: string) => {
const baseElement = this.getBaseElement();

baseElement.setAttribute('target', value);
};

private getBaseElement() {
const head = globalThis.document.head;

const maybeExistingBase = head.querySelector('base');

if (!maybeExistingBase) {
const newBase = globalThis.document.createElement('base');

head.appendChild(newBase);

return newBase;
}

return maybeExistingBase;
}
}
57 changes: 4 additions & 53 deletions src/iframe/IFrameController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import debounce from 'lodash.debounce';

import {Disposable, updateClassNames, updateStyles} from '../utils';
import {BaseIFrameController} from './BaseIFrameController';

const DEFAULT_RESIZE_DELAY = 150;

Expand All @@ -23,19 +23,14 @@ type EventHandlers = {
[K in keyof Events]: Set<Events[K]>;
};

export class IFrameController extends Disposable {
private readonly domContainer: HTMLElement;
private classNames: string[] = [];
export class IFrameController extends BaseIFrameController {
private resizeObserver: ResizeObserver;
private styles: Record<string, string> = {};
private eventHandlers: EventHandlers = {
resize: new Set(),
};

constructor(bodyElement: HTMLElement) {
super();

this.domContainer = bodyElement;
constructor(iframe: HTMLIFrameElement | Window) {
super(iframe);

this.resizeObserver = new ResizeObserver(
debounce(this.dispatchResize, DEFAULT_RESIZE_DELAY),
Expand All @@ -46,34 +41,6 @@ export class IFrameController extends Disposable {
this.dispose.add(() => this.resizeObserver.disconnect());
}

setClassNames = (classNames: string[] | undefined = []) => {
updateClassNames(this.domContainer, classNames, this.classNames);

// update this._classNames to the new classNames
this.classNames = classNames;
};

setStyles = (styles: Record<string, string> | undefined = {}) => {
updateStyles(this.domContainer, styles, this.styles);

// update this._styles to the new styles
this.styles = styles;
};

replaceHTML = (htmlString: string) => {
const fragment = globalThis.document.createRange().createContextualFragment(htmlString);

this.domContainer.replaceChildren(fragment);

// TODO: should we reinitialize Observer?
};

setBaseTarget = (value: string) => {
const baseElement = this.getBaseElement();

baseElement.setAttribute('target', value);
};

on<E extends keyof Events>(eventName: E, eventHandler: Events[E]) {
this.eventHandlers[eventName].add(eventHandler);

Expand All @@ -85,20 +52,4 @@ export class IFrameController extends Disposable {
this.eventHandlers.resize.forEach((handler) => handler(entry.contentRect));
}
};

private getBaseElement() {
const head = globalThis.document.head;

const maybeExistingBase = head.querySelector('base');

if (!maybeExistingBase) {
const newBase = globalThis.document.createElement('base');

head.appendChild(newBase);

return newBase;
}

return maybeExistingBase;
}
}
20 changes: 12 additions & 8 deletions src/iframe/NoScriptIFrameController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import debounce from 'lodash.debounce';

import {Disposable} from '../utils';
import {BaseIFrameController} from './BaseIFrameController';

const DEFAULT_RESIZE_DELAY = 150;

type ResizeListener = (value: number) => void;

export class NoScriptIFrameController extends Disposable {
export class NoScriptIFrameController extends BaseIFrameController {
private static observables = new Set<NoScriptIFrameController>();
private static resizeCheckerActive = false;

Expand All @@ -27,6 +27,11 @@ export class NoScriptIFrameController extends Disposable {
}
}

// rAF has a separate place in the event loop and is called only when
// the browser is ready to repaint. The browser itself makes sure
// not to call it for background tabs or frames out of viewport.
// The size recalculation is only needed along with the display update,
// so this is the most appropriate place in the event loop.
requestAnimationFrame(NoScriptIFrameController.checkResize);
}

Expand All @@ -39,15 +44,14 @@ export class NoScriptIFrameController extends Disposable {
NoScriptIFrameController.checkResize();
}

private domContainer: HTMLIFrameElement;
private resizeObserver: ResizeObserver | undefined;
private lastHeight = 0;
private resizeListener: ResizeListener | undefined;

constructor(frame: HTMLIFrameElement) {
super();

this.domContainer = frame;
// The controller is not useless, as there is a narrower type of argument here.
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(iframe: HTMLIFrameElement) {
super(iframe);
}

setResizeListener(listener: ResizeListener) {
Expand All @@ -67,7 +71,7 @@ export class NoScriptIFrameController extends Disposable {
}

updateHeight() {
const frameWindow = this.domContainer.contentWindow;
const frameWindow = (this.iframe as HTMLIFrameElement).contentWindow;

if (!frameWindow || !this.resizeListener) {
return;
Expand Down
2 changes: 1 addition & 1 deletion src/iframe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type {Commands, Events} from './IFrameController';
const $$PublisherInstanceSymbol = Symbol.for('$$RPCAPIPublisher');

const onDOMReady = () => {
const controller = new IFrameController(globalThis.document.body);
const controller = new IFrameController(globalThis.window);
const publisher = new APIPublisher(new PostMessageChannel(globalThis.parent));

publisher
Expand Down
27 changes: 15 additions & 12 deletions src/runtime/SrcDocIFrameController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type {BaseIFrameController} from '../iframe/BaseIFrameController';

import {Deferred, Disposable, TaskQueue} from '../utils';
import {isNoScriptIFrame} from '../utils/isNoScriptIFrame';
import {IFrameController} from '../iframe/IFrameController';
Expand Down Expand Up @@ -58,7 +60,7 @@ export class SrcDocIFrameController extends Disposable implements IEmbeddedConte
private readonly taskQueue: TaskQueue;
private readonly controllerInitialiazedFuse = new Deferred<void>();

private iframeController: IFrameController | null = null;
private iframeController: BaseIFrameController | null = null;

constructor(host: HTMLElement, config: EmbedsConfig) {
validateHostElement(host);
Expand Down Expand Up @@ -140,22 +142,23 @@ export class SrcDocIFrameController extends Disposable implements IEmbeddedConte
private async instantiateController() {
await ensureIframeLoaded(this.host);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const controller = new IFrameController(this.host.contentWindow!.document.body);
if (isNoScriptIFrame(this.host)) {
const controller = new NoScriptIFrameController(this.host);

this.iframeController = controller;
this.dispose.add(() => controller.dispose());
this.iframeController = controller;

if (isNoScriptIFrame(this.host)) {
const noScriptIFrameController = new NoScriptIFrameController(this.host);
this.dispose.add(() => controller.dispose());

this.dispose.add(() => noScriptIFrameController.dispose());
this.dispose.add(
noScriptIFrameController.setResizeListener((value) =>
this.updateIFrameHeight(value),
),
controller.setResizeListener((value) => this.updateIFrameHeight(value)),
);
} else {
const controller = new IFrameController(this.host);

this.iframeController = controller;

this.dispose.add(() => controller.dispose());

this.dispose.add(
controller.on('resize', (value) => this.updateIFrameHeight(value.height)),
);
Expand All @@ -164,7 +167,7 @@ export class SrcDocIFrameController extends Disposable implements IEmbeddedConte
return this.controllerInitialiazedFuse.resolve();
}

private executeOnController(execution: (controller: IFrameController) => void) {
private executeOnController(execution: (controller: BaseIFrameController) => void) {
return this.taskQueue.run(async () => {
if (this.iframeController === null) {
throw new Error(
Expand Down

0 comments on commit e4e6d99

Please sign in to comment.