@readium/navigator
Version:
Next generation SDK for publications in Web Apps
201 lines (176 loc) • 7.9 kB
text/typescript
import { Loader, ModuleName } from "@readium/navigator-html-injectables";
import { FrameComms } from "../epub/frame/FrameComms.ts";
import type { ReadiumWindow } from "../../../navigator-html-injectables/types/src/helpers/dom";
import { sML } from "@readium/navigator-html-injectables";
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator.ts";
import { KeyboardConditionBridge } from "../peripherals/KeyboardConditionBridge.ts";
export class WebPubFrameManager {
private frame: HTMLIFrameElement;
private loader: Loader | undefined;
public readonly source: string;
private comms: FrameComms | undefined;
private hidden: boolean = true;
private destroyed: boolean = false;
private readonly contentProtectionConfig: IContentProtectionConfig;
private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
private conditionBridge?: KeyboardConditionBridge;
private currModules: ModuleName[] = [];
constructor(
source: string,
contentProtectionConfig: IContentProtectionConfig = {},
keyboardPeripheralsConfig: IKeyboardPeripheralsConfig = []
) {
this.frame = document.createElement("iframe");
this.frame.classList.add("readium-navigator-iframe");
this.frame.style.visibility = "hidden";
this.frame.style.setProperty("aria-hidden", "true");
this.frame.style.opacity = "0";
this.frame.style.position = "absolute";
this.frame.style.pointerEvents = "none";
this.frame.style.transition = "visibility 0s, opacity 0.1s linear";
// Protect against background color bleeding
this.frame.style.backgroundColor = "#FFFFFF";
this.source = source;
// Use the provided content protection config directly without overriding defaults
this.contentProtectionConfig = { ...contentProtectionConfig };
this.keyboardPeripheralsConfig = [...keyboardPeripheralsConfig];
}
async load(modules: ModuleName[] = []): Promise<Window> {
return new Promise((res, rej) => {
if (this.loader) {
const wnd = this.frame.contentWindow!;
if ([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
try { res(wnd); } catch (error) {};
return;
}
this.comms?.halt();
this.loader.destroy();
this.loader = new Loader(wnd as ReadiumWindow, modules);
this.currModules = modules;
this.comms = undefined;
try { res(wnd); } catch (error) {}
return;
}
this.frame.onload = () => {
const wnd = this.frame.contentWindow!;
this.loader = new Loader(wnd as ReadiumWindow, modules);
this.currModules = modules;
try { res(wnd); } catch (error) {}
};
this.frame.onerror = (err) => {
try { rej(err); } catch (error) {}
}
this.frame.contentWindow!.location.replace(this.source);
});
}
private applyContentProtection() {
if (!this.comms) this.comms!.resume();
// Send content protection config
this.comms!.send("peripherals_protection", this.contentProtectionConfig);
// Send keyboard peripherals, filtered through condition bridge
if (this.keyboardPeripheralsConfig && this.keyboardPeripheralsConfig.length > 0) {
this.conditionBridge?.destroy();
this.conditionBridge = new KeyboardConditionBridge(
this.keyboardPeripheralsConfig,
(serializable) => {
if (serializable.length > 0)
this.comms!.send("keyboard_peripherals", serializable);
}
);
this.conditionBridge.setup();
}
// Apply scroll protection if enabled
if (this.contentProtectionConfig.monitorScrollingExperimental) {
this.comms!.send("scroll_protection", {});
}
// Apply print protection if configured
if (this.contentProtectionConfig.protectPrinting?.disable) {
this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
}
}
async destroy() {
this.conditionBridge?.destroy();
await this.hide();
this.loader?.destroy();
this.frame.remove();
this.destroyed = true;
}
async hide(): Promise<void> {
if(this.destroyed) return;
this.frame.style.visibility = "hidden";
this.frame.style.setProperty("aria-hidden", "true");
this.frame.style.opacity = "0";
this.frame.style.pointerEvents = "none";
this.hidden = true;
if(this.frame.parentElement) {
if(this.comms === undefined || !this.comms.ready) return;
return new Promise((res, _) => {
this.comms?.send("unfocus", undefined, (_: boolean) => {
this.comms?.halt();
res();
});
});
} else {
this.comms?.halt();
}
}
async show(atProgress?: number): Promise<void> {
if (this.destroyed) throw Error("Trying to show frame when it doesn't exist");
if (!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
if (this.comms) this.comms.resume();
else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
return new Promise((res, _) => {
this.comms?.send("activate", undefined, () => {
this.comms?.send("focus", undefined, () => {
// Apply content protection synchronously
this.applyContentProtection();
const remove = () => {
this.frame.style.removeProperty("visibility");
this.frame.style.removeProperty("aria-hidden");
this.frame.style.removeProperty("opacity");
this.frame.style.removeProperty("pointer-events");
this.hidden = false;
if (sML.UA.WebKit) {
this.comms?.send("force_webkit_recalc", undefined);
}
res();
}
if (atProgress !== undefined) {
this.comms?.send("go_progression", atProgress, remove);
} else {
remove();
}
});
});
});
}
setCSSProperties(properties: { [key: string]: string }) {
if(this.destroyed || !this.frame.contentWindow) return;
// We need to resume and halt postMessage to update the properties
// if the frame is hidden since it's been halted in hide()
if (this.hidden) {
if (this.comms) this.comms?.resume();
else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
}
this.comms?.send("update_properties", properties);
if (this.hidden) this.comms?.halt();
}
get iframe() {
if(this.destroyed) throw Error("Trying to use frame when it doesn't exist");
return this.frame;
}
get realSize() {
if(this.destroyed) throw Error("Trying to use frame client rect when it doesn't exist");
return this.frame.getBoundingClientRect();
}
get window() {
if(this.destroyed || !this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
return this.frame.contentWindow;
}
get msg() {
return this.comms;
}
get ldr() {
return this.loader;
}
}