UNPKG

@paperbits/core

Version:
180 lines (138 loc) 7.03 kB
import * as ko from "knockout"; import { EventManager, Events, GlobalEventHandler } from "@paperbits/common/events"; import { IComponent, ViewManager, ViewManagerMode } from "@paperbits/common/ui"; import { Router, Route } from "@paperbits/common/routing"; import { MetaDataSetter } from "@paperbits/common/meta/metaDataSetter"; import { SiteService, SiteSettingsContract } from "@paperbits/common/sites"; import { IMediaService } from "@paperbits/common/media"; interface HostConfig { viewport: ko.Observable<string>; block: ko.Observable<boolean>; host: ko.Observable<IComponent>; onDocumentCreated: (document: Document) => void; onDocumentDisposed: () => void; } export class HostBindingHandler { private readonly hostComponent: ko.Observable<any>; private readonly designTime: ko.Observable<boolean>; private onDocumentCreated: (document: Document) => void; private onDocumentDisposed: () => void; constructor( private readonly globalEventHandler: GlobalEventHandler, private readonly viewManager: ViewManager, private readonly router: Router, private readonly siteService: SiteService, private readonly mediaService: IMediaService ) { this.hostComponent = ko.observable(); this.designTime = ko.observable(true); ko.bindingHandlers["host"] = { init: (element: HTMLElement, valueAccessor: () => HostConfig) => { const config = valueAccessor(); const css = ko.observable<string>("desktop"); config.block.subscribe(this.designTime); config.viewport.subscribe((viewport: string) => { this.viewManager.mode = ViewManagerMode.selecting; switch (viewport) { case "zoomout": css("viewport-zoomout"); this.viewManager.mode = ViewManagerMode.pause; break; case "xl": css("viewport-xl"); break; case "lg": css("viewport-lg"); break; case "md": css("viewport-md"); break; case "sm": css("viewport-sm"); break; case "xs": css("viewport-xs"); break; default: throw new Error("Unknown viewport"); } }); this.onDocumentCreated = config.onDocumentCreated; this.onDocumentDisposed = config.onDocumentDisposed; ko.applyBindingsToNode(element, { css: css }, null); const hostElement = this.createIFrame(); element.appendChild(hostElement); }, update: (element: HTMLElement, valueAccessor: any) => { const config = valueAccessor(); this.hostComponent(config.host()); } }; } private createIFrame(): HTMLIFrameElement { const hostElement: HTMLIFrameElement = document.createElement("iframe"); hostElement.src = "/page.html?designtime=true"; hostElement.classList.add("host"); hostElement.title = "Website"; hostElement.tabIndex = -1; hostElement.setAttribute("aria-hidden", "true"); let hostedWindowHistory; const onRouteChange = (route: Route): void => { route.metadata.originatedByHost = true; hostedWindowHistory.pushState(route, route.title, route.url); }; const onLoad = async (): Promise<void> => { const contentDocument = hostElement.contentDocument; if (this.onDocumentCreated) { this.onDocumentCreated(contentDocument); } this.globalEventHandler.appendDocument(contentDocument); this.setRootElement(contentDocument.body); /* intercepting push state of the hosted iframe window */ hostedWindowHistory = hostElement.contentDocument.defaultView.window.history; const hostedWindowOriginalPushState = hostedWindowHistory.pushState; /* adding listener to designer's route handler */ this.router.addRouteChangeListener(onRouteChange); /* pushing initial route to route handler of the hosted iframe window */ const initialRoute = this.router.getCurrentRoute(); hostedWindowHistory.pushState(initialRoute, initialRoute.title, location.href); /* overriding pushState method of the hosted iframe window */ hostedWindowHistory.pushState = (route: Route, title: string, url: string) => { hostedWindowOriginalPushState.call(hostedWindowHistory, route, title, url); /* if event originated in the designer, skip pushing the state to avoid circular calls */ if (route && route.metadata.originatedByHost) { return; } /* synching the state with designer's window */ window.history.pushState(route, route.title, route.url); }; const settings = await this.siteService.getSettings<any>(); const siteSettings: SiteSettingsContract = settings?.site; if (!siteSettings?.faviconSourceKey) { return; } const mediaContract = await this.mediaService.getMediaByKey(siteSettings.faviconSourceKey); if (!mediaContract || !mediaContract.permalink) { return; } MetaDataSetter.setFavIcon(mediaContract.permalink); }; const onUnload = (): void => { /* removing listener when iframe gets unloaded */ this.router.removeRouteChangeListener(onRouteChange); if (this.onDocumentDisposed) { this.onDocumentDisposed(); } }; hostElement.addEventListener("load", onLoad, false); hostElement.addEventListener("beforeunload", onUnload, false); return hostElement; } private setRootElement(bodyElement: HTMLElement): void { const styleElement = document.createElement("style"); bodyElement.ownerDocument.head.appendChild(styleElement); ko.applyBindingsToNode(bodyElement, { css: { design: this.designTime } }, null); ko.applyBindingsToNode(styleElement, { styleSheet: {} }, null); ko.applyBindingsToNode(bodyElement, { component: this.hostComponent }, null); } }