UNPKG

zoid

Version:
1,192 lines (989 loc) 46.6 kB
/* @flow */ /* eslint max-lines: 0 */ import { send, bridge, ProxyWindow, toProxyWindow, type CrossDomainFunctionType, cleanUpWindow } from 'post-robot/src'; import { isSameDomain, matchDomain, getDomainFromUrl, isBlankDomain, getAncestor, getDomain, type CrossDomainWindowType, getDistanceFromTop, normalizeMockUrl, assertSameDomain, closeWindow, onCloseWindow, isWindowClosed, isSameTopWindow, type DomainMatcher } from 'cross-domain-utils/src'; import { ZalgoPromise } from 'zalgo-promise/src'; import { addEventListener, uniqueID, elementReady, writeElementToWindow, eventEmitter, type EventEmitterType, noop, onResize, extendUrl, appendChild, cleanup, once, stringifyError, destroyElement, getElementSafe, showElement, hideElement, iframe, memoize, isElementClosed, awaitFrameWindow, popup, normalizeDimension, watchElementForClose, isShadowElement, insertShadowSlot, extend } from 'belter/src'; import { ZOID, POST_MESSAGE, CONTEXT, EVENT, METHOD, WINDOW_REFERENCE, DEFAULT_DIMENSIONS } from '../constants'; import { getGlobal, getProxyObject, crossDomainSerialize, buildChildWindowName, type ProxyObject } from '../lib'; import type { PropsInputType, PropsType } from '../component/props'; import type { ChildExportsType } from '../child'; import type { CssDimensionsType, ContainerReferenceType } from '../types'; import type { NormalizedComponentOptionsType, AttributesType } from '../component'; import { serializeProps, extendProps } from './props'; export type RenderOptionsType<P> = {| uid : string, props : PropsType<P>, tag : string, context : $Values<typeof CONTEXT>, close : (?string) => ZalgoPromise<void>, focus : () => ZalgoPromise<void>, doc : Document, container? : HTMLElement, dimensions : CssDimensionsType, state : Object, event : EventEmitterType, frame : ?HTMLIFrameElement, prerenderFrame : ?HTMLIFrameElement |}; export type ParentExportsType<P, X> = {| init : (ChildExportsType<P>) => ZalgoPromise<void>, close : () => ZalgoPromise<void>, checkClose : CrossDomainFunctionType<[], boolean>, resize : CrossDomainFunctionType<[{| width? : ?number, height? : ?number |}], void>, onError : (mixed) => ZalgoPromise<void>, show : () => ZalgoPromise<void>, hide : () => ZalgoPromise<void>, export : (X) => ZalgoPromise<void> |}; export type WindowRef = {| type : typeof WINDOW_REFERENCE.OPENER |} | {| type : typeof WINDOW_REFERENCE.PARENT, distance : number |} | {| type : typeof WINDOW_REFERENCE.GLOBAL, uid : string |} | {| type : typeof WINDOW_REFERENCE.NAME, name : string |}; export type InitialChildPayload<P, X> = {| uid : string, tag : string, version : string, context : $Values<typeof CONTEXT>, childDomainMatch : DomainMatcher, props : PropsType<P>, exports : ParentExportsType<P, X> |}; export type InitialChildPayloadMetadata = {| windowRef : WindowRef |}; export type StateType = Object; export type ParentHelpers<P> = {| state : StateType, close : () => ZalgoPromise<void>, focus : () => ZalgoPromise<void>, resize : ({| width : ?number, height : ?number |}) => ZalgoPromise<void>, onError : (mixed) => ZalgoPromise<void>, updateProps : PropsInputType<P> => ZalgoPromise<void>, event : EventEmitterType, show : () => ZalgoPromise<void>, hide : () => ZalgoPromise<void> |}; function getDefaultProps<P>() : PropsType<P> { // $FlowFixMe return {}; } type InternalState = {| visible : boolean |}; type Rerender = () => ZalgoPromise<void>; type RenderContainerOptions = {| context : $Values<typeof CONTEXT>, proxyFrame : ?ProxyObject<HTMLIFrameElement>, proxyPrerenderFrame : ?ProxyObject<HTMLIFrameElement>, rerender : Rerender |}; type ResolveInitPromise = () => ZalgoPromise<void>; type RejectInitPromise = (mixed) => ZalgoPromise<void>; type GetProxyContainer = (container : ContainerReferenceType) => ZalgoPromise<ProxyObject<HTMLElement>>; type Show = () => ZalgoPromise<void>; type Hide = () => ZalgoPromise<void>; type Close = () => ZalgoPromise<void>; type OnError = (mixed) => ZalgoPromise<void>; type RenderContainer = (proxyContainer : ProxyObject<HTMLElement>, RenderContainerOptions) => ZalgoPromise<?ProxyObject<HTMLElement>>; type SetProxyWin = (ProxyWindow) => ZalgoPromise<void>; type GetProxyWindow = () => ZalgoPromise<ProxyWindow>; type OpenFrame = (context : $Values<typeof CONTEXT>, {| windowName : string |}) => ZalgoPromise<?ProxyObject<HTMLIFrameElement>>; type OpenPrerenderFrame = (context : $Values<typeof CONTEXT>) => ZalgoPromise<?ProxyObject<HTMLIFrameElement>>; type Prerender = (proxyPrerenderWin : ProxyWindow, {| context : $Values<typeof CONTEXT>|}) => ZalgoPromise<void>; type Open = (context : $Values<typeof CONTEXT>, {| proxyWin : ProxyWindow, proxyFrame : ?ProxyObject<HTMLIFrameElement>, windowName : string |}) => ZalgoPromise<ProxyWindow>; type OpenPrerender = (context : $Values<typeof CONTEXT>, proxyWin : ProxyWindow, proxyPrerenderFrame : ?ProxyObject<HTMLIFrameElement>) => ZalgoPromise<ProxyWindow>; type WatchForUnload = () => ZalgoPromise<void>; type GetInternalState = () => ZalgoPromise<InternalState>; type SetInternalState = (InternalState) => ZalgoPromise<InternalState>; type ParentDelegateOverrides<P> = {| props : PropsType<P>, event : EventEmitterType, close : Close, onError : OnError, getProxyContainer : GetProxyContainer, show : Show, hide : Hide, renderContainer : RenderContainer, getProxyWindow : GetProxyWindow, setProxyWin : SetProxyWin, openFrame : OpenFrame, openPrerenderFrame : OpenPrerenderFrame, prerender : Prerender, open : Open, openPrerender : OpenPrerender, watchForUnload : WatchForUnload, getInternalState : GetInternalState, setInternalState : SetInternalState, resolveInitPromise : ResolveInitPromise, rejectInitPromise : RejectInitPromise |}; type DelegateOverrides = {| getProxyContainer : GetProxyContainer, show : Show, hide : Hide, renderContainer : RenderContainer, getProxyWindow : GetProxyWindow, setProxyWin : SetProxyWin, openFrame : OpenFrame, openPrerenderFrame : OpenPrerenderFrame, prerender : Prerender, open : Open, openPrerender : OpenPrerender, watchForUnload : WatchForUnload |}; type RenderOptions = {| target : CrossDomainWindowType, container : ContainerReferenceType, context : $Values<typeof CONTEXT>, rerender : Rerender |}; type ParentComponent<P, X> = {| init : () => void, render : (RenderOptions) => ZalgoPromise<void>, getProps : () => PropsType<P>, setProps : (newProps : PropsInputType<P>, isUpdate? : boolean) => void, export : (X) => ZalgoPromise<void>, destroy : (err? : mixed) => ZalgoPromise<void>, getHelpers : () => ParentHelpers<P>, getDelegateOverrides : () => ZalgoPromise<DelegateOverrides>, getExports : () => X |}; const getDefaultOverrides = <P>() : ParentDelegateOverrides<P> => { // $FlowFixMe return {}; }; type ParentOptions<P, X, C> = {| uid : string, options : NormalizedComponentOptionsType<P, X, C>, overrides? : ParentDelegateOverrides<P>, parentWin? : CrossDomainWindowType |}; export function parentComponent<P, X, C>({ uid, options, overrides = getDefaultOverrides(), parentWin = window } : ParentOptions<P, X, C>) : ParentComponent<P, X> { const { propsDef, containerTemplate, prerenderTemplate, tag, name, attributes, dimensions, autoResize, url, domain: domainMatch, validate, exports: xports } = options; const initPromise = new ZalgoPromise(); const handledErrors = []; const clean = cleanup(); const state = {}; const inputProps = {}; let internalState = { visible: true }; const event = overrides.event ? overrides.event : eventEmitter(); const props : PropsType<P> = overrides.props ? overrides.props : getDefaultProps(); let currentProxyWin : ?ProxyWindow; let currentProxyContainer : ?ProxyObject<HTMLElement>; let childComponent : ?ChildExportsType<P>; let currentChildDomain : ?string; let currentContainer : HTMLElement | void; const onErrorOverride : ?OnError = overrides.onError; let getProxyContainerOverride : ?GetProxyContainer = overrides.getProxyContainer; let showOverride : ?Show = overrides.show; let hideOverride : ?Hide = overrides.hide; const closeOverride : ?Close = overrides.close; let renderContainerOverride : ?RenderContainer = overrides.renderContainer; let getProxyWindowOverride : ?GetProxyWindow = overrides.getProxyWindow; let setProxyWinOverride : ?SetProxyWin = overrides.setProxyWin; let openFrameOverride : ?OpenFrame = overrides.openFrame; let openPrerenderFrameOverride : ?OpenPrerenderFrame = overrides.openPrerenderFrame; let prerenderOverride : ?Prerender = overrides.prerender; let openOverride : ?Open = overrides.open; let openPrerenderOverride : ?OpenPrerender = overrides.openPrerender; let watchForUnloadOverride : ?WatchForUnload = overrides.watchForUnload; const getInternalStateOverride : ?GetInternalState = overrides.getInternalState; const setInternalStateOverride : ?SetInternalState = overrides.setInternalState; const getDimensions = () : CssDimensionsType => { if (typeof dimensions === 'function') { return dimensions({ props }); } return dimensions; }; const resolveInitPromise = () => { return ZalgoPromise.try(() => { if (overrides.resolveInitPromise) { return overrides.resolveInitPromise(); } return initPromise.resolve(); }); }; const rejectInitPromise = (err : mixed) => { return ZalgoPromise.try(() => { if (overrides.rejectInitPromise) { return overrides.rejectInitPromise(err); } return initPromise.reject(err); }); }; const getPropsForChild = (initialChildDomain : string) : ZalgoPromise<PropsType<P>> => { const result = {}; for (const key of Object.keys(props)) { const prop = propsDef[key]; if (prop && prop.sendToChild === false) { continue; } if (prop && prop.sameDomain && !matchDomain(initialChildDomain, getDomain(window))) { continue; } result[key] = props[key]; } // $FlowFixMe return ZalgoPromise.hash(result); }; const setupEvents = () => { event.on(EVENT.RENDER, () => props.onRender()); event.on(EVENT.DISPLAY, () => props.onDisplay()); event.on(EVENT.RENDERED, () => props.onRendered()); event.on(EVENT.CLOSE, () => props.onClose()); event.on(EVENT.DESTROY, () => props.onDestroy()); event.on(EVENT.RESIZE, () => props.onResize()); event.on(EVENT.FOCUS, () => props.onFocus()); event.on(EVENT.PROPS, (newProps) => props.onProps(newProps)); event.on(EVENT.ERROR, err => { if (props && props.onError) { return props.onError(err); } else { return rejectInitPromise(err).then(() => { setTimeout(() => { throw err; }, 1); }); } }); clean.register(event.reset); }; const getInternalState = () => { return ZalgoPromise.try(() => { if (getInternalStateOverride) { return getInternalStateOverride(); } return internalState; }); }; const setInternalState = (newInternalState) => { return ZalgoPromise.try(() => { if (setInternalStateOverride) { return setInternalStateOverride(newInternalState); } internalState = { ...internalState, ...newInternalState }; return internalState; }); }; const getProxyWindow = () : ZalgoPromise<ProxyWindow> => { if (getProxyWindowOverride) { return getProxyWindowOverride(); } return ZalgoPromise.try(() => { const windowProp = props.window; if (windowProp) { const proxyWin = toProxyWindow(windowProp); clean.register(() => windowProp.close()); return proxyWin; } return new ProxyWindow({ send }); }); }; const setProxyWin = (proxyWin : ProxyWindow) : ZalgoPromise<void> => { if (setProxyWinOverride) { return setProxyWinOverride(proxyWin); } return ZalgoPromise.try(() => { currentProxyWin = proxyWin; }); }; const show = () : ZalgoPromise<void> => { if (showOverride) { return showOverride(); } return ZalgoPromise.hash({ setState: setInternalState({ visible: true }), showElement: currentProxyContainer ? currentProxyContainer.get().then(showElement) : null }).then(noop); }; const hide = () : ZalgoPromise<void> => { if (hideOverride) { return hideOverride(); } return ZalgoPromise.hash({ setState: setInternalState({ visible: false }), showElement: currentProxyContainer ? currentProxyContainer.get().then(hideElement) : null }).then(noop); }; const getUrl = () : string => { if (typeof url === 'function') { return url({ props }); } return url; }; const getAttributes = () : AttributesType => { if (typeof attributes === 'function') { return attributes({ props }); } return attributes; }; const buildQuery = () : ZalgoPromise<{| [string] : string | boolean |}> => { return serializeProps(propsDef, props, METHOD.GET); }; const buildBody = () : ZalgoPromise<{| [string] : string | boolean |}> => { return serializeProps(propsDef, props, METHOD.POST); }; const buildUrl = () : ZalgoPromise<string> => { return buildQuery().then(query => { return extendUrl(normalizeMockUrl(getUrl()), { query }); }); }; const getInitialChildDomain = () : string => { return getDomainFromUrl(getUrl()); }; const getDomainMatcher = () : DomainMatcher => { if (domainMatch) { return domainMatch; } return getInitialChildDomain(); }; const openFrame = (context : $Values<typeof CONTEXT>, { windowName } : {| windowName : string |}) : ZalgoPromise<?ProxyObject<HTMLIFrameElement>> => { if (openFrameOverride) { return openFrameOverride(context, { windowName }); } return ZalgoPromise.try(() => { if (context === CONTEXT.IFRAME && __ZOID__.__IFRAME_SUPPORT__) { // $FlowFixMe const attrs = { name: windowName, title: name, ...getAttributes().iframe }; return getProxyObject(iframe({ attributes: attrs })); } }); }; const openPrerenderFrame = (context : $Values<typeof CONTEXT>) : ZalgoPromise<?ProxyObject<HTMLIFrameElement>> => { if (openPrerenderFrameOverride) { return openPrerenderFrameOverride(context); } return ZalgoPromise.try(() => { if (context === CONTEXT.IFRAME && __ZOID__.__IFRAME_SUPPORT__) { // $FlowFixMe const attrs = { name: `__${ ZOID }_prerender_frame__${ name }_${ uniqueID() }__`, title: `prerender__${ name }`, ...getAttributes().iframe }; return getProxyObject(iframe({ attributes: attrs })); } }); }; const openPrerender = (context : $Values<typeof CONTEXT>, proxyWin : ProxyWindow, proxyPrerenderFrame : ?ProxyObject<HTMLIFrameElement>) : ZalgoPromise<ProxyWindow> => { if (openPrerenderOverride) { return openPrerenderOverride(context, proxyWin, proxyPrerenderFrame); } return ZalgoPromise.try(() => { if (context === CONTEXT.IFRAME && __ZOID__.__IFRAME_SUPPORT__) { if (!proxyPrerenderFrame) { throw new Error(`Expected proxy frame to be passed`); } return proxyPrerenderFrame.get().then(prerenderFrame => { clean.register(() => destroyElement(prerenderFrame)); return awaitFrameWindow(prerenderFrame).then(prerenderFrameWindow => { return assertSameDomain(prerenderFrameWindow); }).then(win => { return toProxyWindow(win); }); }); } else if (context === CONTEXT.POPUP && __ZOID__.__POPUP_SUPPORT__) { return proxyWin; } else { throw new Error(`No render context available for ${ context }`); } }); }; const focus = () : ZalgoPromise<void> => { return ZalgoPromise.try(() => { if (currentProxyWin) { return ZalgoPromise.all([ event.trigger(EVENT.FOCUS), currentProxyWin.focus() ]).then(noop); } }); }; const getCurrentWindowReferenceUID = () : string => { const global = getGlobal(window); global.windows = global.windows || {}; global.windows[uid] = window; clean.register(() => { delete global.windows[uid]; }); return uid; }; const getWindowRef = (target : CrossDomainWindowType, initialChildDomain : string, context : $Values<typeof CONTEXT>, proxyWin : ProxyWindow) : WindowRef => { if (initialChildDomain === getDomain(window)) { return { type: WINDOW_REFERENCE.GLOBAL, uid: getCurrentWindowReferenceUID() }; } if (target !== window) { throw new Error(`Can not construct cross-domain window reference for different target window`); } if (props.window) { const actualComponentWindow = proxyWin.getWindow(); if (!actualComponentWindow) { throw new Error(`Can not construct cross-domain window reference for lazy window prop`); } if (getAncestor(actualComponentWindow) !== window) { throw new Error(`Can not construct cross-domain window reference for window prop with different ancestor`); } } if (context === CONTEXT.POPUP) { return { type: WINDOW_REFERENCE.OPENER }; } else if (context === CONTEXT.IFRAME) { return { type: WINDOW_REFERENCE.PARENT, distance: getDistanceFromTop(window) }; } throw new Error(`Can not construct window reference for child`); }; const runTimeout = () : ZalgoPromise<void> => { return ZalgoPromise.try(() => { const timeout = props.timeout; if (timeout) { return initPromise.timeout(timeout, new Error(`Loading component timed out after ${ timeout } milliseconds`)); } }); }; const initChild = (childDomain : string, childExports : ChildExportsType<P>) : ZalgoPromise<void> => { return ZalgoPromise.try(() => { currentChildDomain = childDomain; childComponent = childExports; resolveInitPromise(); clean.register(() => childExports.close.fireAndForget().catch(noop)); }); }; const resize = ({ width, height } : {| width? : ?number, height? : ?number |}) : ZalgoPromise<void> => { return ZalgoPromise.try(() => { event.trigger(EVENT.RESIZE, { width, height }); }); }; const destroy = (err : mixed) : ZalgoPromise<void> => { // eslint-disable-next-line promise/no-promise-in-callback return ZalgoPromise.try(() => { return event.trigger(EVENT.DESTROY); }).catch(noop).then(() => { return clean.all(err); }).then(() => { initPromise.asyncReject(err || new Error('Component destroyed')); }); }; const close = memoize((err? : mixed) : ZalgoPromise<void> => { return ZalgoPromise.try(() => { if (closeOverride) { // $FlowFixMe const source = closeOverride.__source__; if (isWindowClosed(source)) { return; } return closeOverride(); } return ZalgoPromise.try(() => { return event.trigger(EVENT.CLOSE); }).then(() => { return destroy(err || new Error(`Component closed`)); }); }); }); const open = (context : $Values<typeof CONTEXT>, { proxyWin, proxyFrame, windowName } : {| proxyWin : ProxyWindow, proxyFrame : ?ProxyObject<HTMLIFrameElement>, windowName : string |}) : ZalgoPromise<ProxyWindow> => { if (openOverride) { return openOverride(context, { proxyWin, proxyFrame, windowName }); } return ZalgoPromise.try(() => { if (context === CONTEXT.IFRAME && __ZOID__.__IFRAME_SUPPORT__) { if (!proxyFrame) { throw new Error(`Expected proxy frame to be passed`); } return proxyFrame.get().then(frame => { return awaitFrameWindow(frame).then(win => { clean.register(() => destroyElement(frame)); clean.register(() => cleanUpWindow(win)); return win; }); }); } else if (context === CONTEXT.POPUP && __ZOID__.__POPUP_SUPPORT__) { let { width = DEFAULT_DIMENSIONS.WIDTH, height = DEFAULT_DIMENSIONS.HEIGHT } = getDimensions(); width = normalizeDimension(width, window.outerWidth); height = normalizeDimension(height, window.outerWidth); // $FlowFixMe const attrs = { name: windowName, width, height, ...getAttributes().popup }; const win = popup('', attrs); clean.register(() => closeWindow(win)); clean.register(() => cleanUpWindow(win)); return win; } else { throw new Error(`No render context available for ${ context }`); } }).then(win => { proxyWin.setWindow(win, { send }); return proxyWin.setName(windowName).then(() => { return proxyWin; }); }); }; const watchForUnload = () => { return ZalgoPromise.try(() => { const unloadWindowListener = addEventListener(window, 'unload', once(() => { destroy(new Error(`Window navigated away`)); })); const closeParentWindowListener = onCloseWindow(parentWin, destroy, 3000); clean.register(closeParentWindowListener.cancel); clean.register(unloadWindowListener.cancel); if (watchForUnloadOverride) { return watchForUnloadOverride(); } }); }; const watchForClose = (proxyWin : ProxyWindow, context : $Values<typeof CONTEXT>) : ZalgoPromise<void> => { let cancelled = false; clean.register(() => { cancelled = true; }); return ZalgoPromise.delay(2000).then(() => { return proxyWin.isClosed(); }).then(isClosed => { if (!cancelled) { if (isClosed) { return close(new Error(`Detected ${ context } close`)); } else { return watchForClose(proxyWin, context); } } }); }; const checkWindowClose = (proxyWin : ProxyWindow) : ZalgoPromise<boolean> => { let closed = false; return proxyWin.isClosed().then(isClosed => { if (isClosed) { closed = true; return close(new Error(`Detected component window close`)); } return ZalgoPromise.delay(200) .then(() => proxyWin.isClosed()) .then(secondIsClosed => { if (secondIsClosed) { closed = true; return close(new Error(`Detected component window close`)); } }); }).then(() => { return closed; }); }; const onError = (err : mixed) : ZalgoPromise<void> => { if (onErrorOverride) { return onErrorOverride(err); } return ZalgoPromise.try(() => { if (handledErrors.indexOf(err) !== -1) { return; } handledErrors.push(err); initPromise.asyncReject(err); return event.trigger(EVENT.ERROR, err); }); }; const exportsPromise : ZalgoPromise<X> = new ZalgoPromise(); const getExports = () : X => { return xports({ getExports: () => exportsPromise }); }; const xport = (actualExports : X) : ZalgoPromise<void> => { return ZalgoPromise.try(() => { exportsPromise.resolve(actualExports); }); }; initChild.onError = onError; const buildParentExports = (win : ProxyWindow) : ParentExportsType<P, X> => { const checkClose = () => checkWindowClose(win); function init(childExports : ChildExportsType<P>) : ZalgoPromise<void> { return initChild(this.origin, childExports); } return { init, close, checkClose, resize, onError, show, hide, export: xport }; }; const buildInitialChildPayload = ({ proxyWin, initialChildDomain, childDomainMatch, context } : {| proxyWin : ProxyWindow, initialChildDomain : string, childDomainMatch : DomainMatcher, context : $Values<typeof CONTEXT>|} = {}) : ZalgoPromise<InitialChildPayload<P, X>> => { return getPropsForChild(initialChildDomain).then(childProps => { return { uid, context, tag, childDomainMatch, version: __ZOID__.__VERSION__, props: childProps, exports: buildParentExports(proxyWin) }; }); }; const buildSerializedChildPayload = ({ proxyWin, initialChildDomain, childDomainMatch, target = window, context } : {| proxyWin : ProxyWindow, initialChildDomain : string, childDomainMatch : DomainMatcher, target : CrossDomainWindowType, context : $Values<typeof CONTEXT>|} = {}) : ZalgoPromise<string> => { return buildInitialChildPayload({ proxyWin, initialChildDomain, childDomainMatch, context }).then(childPayload => { const { serializedData, cleanReference } = crossDomainSerialize({ data: childPayload, metaData: { windowRef: getWindowRef(target, initialChildDomain, context, proxyWin) }, sender: { domain: getDomain(window) }, receiver: { win: proxyWin, domain: childDomainMatch }, passByReference: initialChildDomain === getDomain() }); clean.register(cleanReference); return serializedData; }); }; const buildWindowName = ({ proxyWin, initialChildDomain, childDomainMatch, target, context } : {| proxyWin : ProxyWindow, initialChildDomain : string, childDomainMatch : DomainMatcher, target : CrossDomainWindowType, context : $Values<typeof CONTEXT>|}) : ZalgoPromise<string> => { return buildSerializedChildPayload({ proxyWin, initialChildDomain, childDomainMatch, target, context }).then(serializedPayload => { return buildChildWindowName({ name, serializedPayload }); }); }; const renderTemplate = (renderer : (RenderOptionsType<P>) => ?HTMLElement, { context, container, doc, frame, prerenderFrame } : {| context : $Values<typeof CONTEXT>, container? : HTMLElement, doc : Document, frame? : ?HTMLIFrameElement, prerenderFrame? : ?HTMLIFrameElement |}) : ?HTMLElement => { return renderer({ uid, container, context, doc, frame, prerenderFrame, focus, close, state, props, tag, dimensions: getDimensions(), event }); }; const prerender = (proxyPrerenderWin : ProxyWindow, { context } : {| context : $Values<typeof CONTEXT>|}) : ZalgoPromise<void> => { if (prerenderOverride) { return prerenderOverride(proxyPrerenderWin, { context }); } return ZalgoPromise.try(() => { if (!prerenderTemplate) { return; } let prerenderWindow = proxyPrerenderWin.getWindow(); if (!prerenderWindow || !isSameDomain(prerenderWindow) || !isBlankDomain(prerenderWindow)) { return; } prerenderWindow = assertSameDomain(prerenderWindow); const doc = prerenderWindow.document; const el = renderTemplate(prerenderTemplate, { context, doc }); if (!el) { return; } if (el.ownerDocument !== doc) { throw new Error(`Expected prerender template to have been created with document from child window`); } writeElementToWindow(prerenderWindow, el); let { width = false, height = false, element = 'body' } = autoResize; element = getElementSafe(element, doc); if (element && (width || height)) { const prerenderResizeListener = onResize(element, ({ width: newWidth, height: newHeight }) => { resize({ width: width ? newWidth : undefined, height: height ? newHeight : undefined }); }, { width, height, win: prerenderWindow }); event.on(EVENT.RENDERED, prerenderResizeListener.cancel); } }); }; const renderContainer : RenderContainer = (proxyContainer : ProxyObject<HTMLElement>, { proxyFrame, proxyPrerenderFrame, context, rerender } : RenderContainerOptions) : ZalgoPromise<?ProxyObject<HTMLElement>> => { if (renderContainerOverride) { return renderContainerOverride(proxyContainer, { proxyFrame, proxyPrerenderFrame, context, rerender }); } return ZalgoPromise.hash({ container: proxyContainer.get(), // $FlowFixMe frame: proxyFrame ? proxyFrame.get() : null, // $FlowFixMe prerenderFrame: proxyPrerenderFrame ? proxyPrerenderFrame.get() : null, internalState: getInternalState() }).then(({ container, frame, prerenderFrame, internalState: { visible } }) => { const innerContainer = renderTemplate(containerTemplate, { context, container, frame, prerenderFrame, doc: document }); if (innerContainer) { if (!visible) { hideElement(innerContainer); } appendChild(container, innerContainer); const containerWatcher = watchElementForClose(innerContainer, () => { const removeError = new Error(`Detected container element removed from DOM`); return ZalgoPromise.delay(1).then(() => { if (isElementClosed(innerContainer)) { close(removeError); } else { clean.all(removeError); return rerender().then(resolveInitPromise, rejectInitPromise); } }); }); clean.register(() => containerWatcher.cancel()); clean.register(() => destroyElement(innerContainer)); currentProxyContainer = getProxyObject(innerContainer); return currentProxyContainer; } }); }; const getBridgeUrl = () : ?string => { if (typeof options.bridgeUrl === 'function') { return options.bridgeUrl({ props }); } return options.bridgeUrl; }; const openBridge = (proxyWin : ProxyWindow, initialChildDomain : string, context : $Values<typeof CONTEXT>) : ?ZalgoPromise<?CrossDomainWindowType> => { if (__POST_ROBOT__.__IE_POPUP_SUPPORT__) { return ZalgoPromise.try(() => { return proxyWin.awaitWindow(); }).then(win => { if (!bridge || !bridge.needsBridge({ win, domain: initialChildDomain }) || bridge.hasBridge(initialChildDomain, initialChildDomain)) { return; } const bridgeUrl = getBridgeUrl(); if (!bridgeUrl) { throw new Error(`Bridge needed to render ${ context }`); } const bridgeDomain = getDomainFromUrl(bridgeUrl); bridge.linkUrl(win, initialChildDomain); return bridge.openBridge(normalizeMockUrl(bridgeUrl), bridgeDomain); }); } }; const getHelpers = () : ParentHelpers<P> => { return { state, event, close, focus, resize, // eslint-disable-next-line no-use-before-define onError, updateProps, show, hide }; }; const getProps = () => props; const getDefaultPropsInput = () : PropsInputType<P> => { // $FlowFixMe return {}; }; const setProps = (newInputProps : PropsInputType<P> = getDefaultPropsInput()) => { if (__DEBUG__ && validate) { validate({ props: newInputProps }); } const container = currentContainer; const helpers = getHelpers(); extend(inputProps, newInputProps); // $FlowFixMe extendProps(propsDef, props, inputProps, helpers, container); }; const updateProps = (newProps : PropsInputType<P>) : ZalgoPromise<void> => { setProps(newProps); return initPromise.then(() => { const child = childComponent; const proxyWin = currentProxyWin; const childDomain = currentChildDomain; if (!child || !proxyWin || !childDomain) { return; } return getPropsForChild(childDomain).then(childProps => { return child.updateProps(childProps).catch(err => { return checkWindowClose(proxyWin).then(closed => { if (!closed) { throw err; } }); }); }); }); }; const getProxyContainer : GetProxyContainer = (container : ContainerReferenceType) : ZalgoPromise<ProxyObject<HTMLElement>> => { if (getProxyContainerOverride) { return getProxyContainerOverride(container); } return ZalgoPromise.try(() => { return elementReady(container); }).then(containerElement => { if (isShadowElement(containerElement)) { containerElement = insertShadowSlot(containerElement); } currentContainer = containerElement; return getProxyObject(containerElement); }); }; const delegate = (context : $Values<typeof CONTEXT>, target : CrossDomainWindowType) : ZalgoPromise<DelegateOverrides> => { const delegateProps = {}; for (const propName of Object.keys(props)) { const propDef = propsDef[propName]; if (propDef && propDef.allowDelegate) { delegateProps[propName] = props[propName]; } } const childOverridesPromise = send(target, `${ POST_MESSAGE.DELEGATE }_${ name }`, { uid, overrides: { props: delegateProps, event, close, onError, getInternalState, setInternalState, resolveInitPromise, rejectInitPromise } }).then(({ data: { parent } }) => { const parentComp : ParentComponent<P, X> = parent; clean.register(err => { if (!isWindowClosed(target)) { return parentComp.destroy(err); } }); return parentComp.getDelegateOverrides(); }).catch(err => { throw new Error(`Unable to delegate rendering. Possibly the component is not loaded in the target window.\n\n${ stringifyError(err) }`); }); getProxyContainerOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.getProxyContainer(...args)); renderContainerOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.renderContainer(...args)); showOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.show(...args)); hideOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.hide(...args)); watchForUnloadOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.watchForUnload(...args)); if (context === CONTEXT.IFRAME && __ZOID__.__IFRAME_SUPPORT__) { getProxyWindowOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.getProxyWindow(...args)); openFrameOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.openFrame(...args)); openPrerenderFrameOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.openPrerenderFrame(...args)); prerenderOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.prerender(...args)); openOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.open(...args)); openPrerenderOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.openPrerender(...args)); } else if (context === CONTEXT.POPUP && __ZOID__.__POPUP_SUPPORT__) { setProxyWinOverride = (...args) => childOverridesPromise.then(childOverrides => childOverrides.setProxyWin(...args)); } return childOverridesPromise; }; const getDelegateOverrides = () : ZalgoPromise<DelegateOverrides> => { return ZalgoPromise.try(() => { return { getProxyContainer, show, hide, renderContainer, getProxyWindow, watchForUnload, openFrame, openPrerenderFrame, prerender, open, openPrerender, setProxyWin }; }); }; const checkAllowRender = (target : CrossDomainWindowType, childDomainMatch : DomainMatcher, container : ContainerReferenceType) => { if (target === window) { return; } if (!isSameTopWindow(window, target)) { throw new Error(`Can only renderTo an adjacent frame`); } const origin = getDomain(); if (!matchDomain(childDomainMatch, origin) && !isSameDomain(target)) { throw new Error(`Can not render remotely to ${ childDomainMatch.toString() } - can only render to ${ origin }`); } if (container && typeof container !== 'string') { throw new Error(`Container passed to renderTo must be a string selector, got ${ typeof container } }`); } }; const init = () => { setupEvents(); }; const render = ({ target, container, context, rerender } : RenderOptions) : ZalgoPromise<void> => { return ZalgoPromise.try(() => { const initialChildDomain = getInitialChildDomain(); const childDomainMatch = getDomainMatcher(); checkAllowRender(target, childDomainMatch, container); const delegatePromise = ZalgoPromise.try(() => { if (target !== window) { return delegate(context, target); } }); const windowProp = props.window; const watchForUnloadPromise = watchForUnload(); const buildBodyPromise = buildBody(); const onRenderPromise = event.trigger(EVENT.RENDER); const getProxyContainerPromise = getProxyContainer(container); const getProxyWindowPromise = getProxyWindow(); const finalSetPropsPromise = getProxyContainerPromise.then(() => { return setProps(); }); const buildUrlPromise = finalSetPropsPromise.then(() => { return buildUrl(); }); const buildWindowNamePromise = getProxyWindowPromise.then(proxyWin => { return buildWindowName({ proxyWin, initialChildDomain, childDomainMatch, target, context }); }); const openFramePromise = buildWindowNamePromise.then(windowName => openFrame(context, { windowName })); const openPrerenderFramePromise = openPrerenderFrame(context); const renderContainerPromise = ZalgoPromise.hash({ proxyContainer: getProxyContainerPromise, proxyFrame: openFramePromise, proxyPrerenderFrame: openPrerenderFramePromise }).then(({ proxyContainer, proxyFrame, proxyPrerenderFrame }) => { return renderContainer(proxyContainer, { context, proxyFrame, proxyPrerenderFrame, rerender }); }).then(proxyContainer => { return proxyContainer; }); const openPromise = ZalgoPromise.hash({ windowName: buildWindowNamePromise, proxyFrame: openFramePromise, proxyWin: getProxyWindowPromise }).then(({ windowName, proxyWin, proxyFrame }) => { return windowProp ? proxyWin : open(context, { windowName, proxyWin, proxyFrame }); }); const openPrerenderPromise = ZalgoPromise.hash({ proxyWin: openPromise, proxyPrerenderFrame: openPrerenderFramePromise }).then(({ proxyWin, proxyPrerenderFrame }) => { return openPrerender(context, proxyWin, proxyPrerenderFrame); }); const setStatePromise = openPromise.then(proxyWin => { currentProxyWin = proxyWin; return setProxyWin(proxyWin); }); const prerenderPromise = ZalgoPromise.hash({ proxyPrerenderWin: openPrerenderPromise, state: setStatePromise }).then(({ proxyPrerenderWin }) => { return prerender(proxyPrerenderWin, { context }); }); const setWindowNamePromise = ZalgoPromise.hash({ proxyWin: openPromise, windowName: buildWindowNamePromise }).then(({ proxyWin, windowName }) => { if (windowProp) { return proxyWin.setName(windowName); } }); const getMethodPromise = ZalgoPromise.hash({ body: buildBodyPromise }).then(({ body }) => { if (options.method) { return options.method; } if (Object.keys(body).length) { return METHOD.POST; } return METHOD.GET; }); const loadUrlPromise = ZalgoPromise.hash({ proxyWin: openPromise, windowUrl: buildUrlPromise, body: buildBodyPromise, method: getMethodPromise, windowName: setWindowNamePromise, prerender: prerenderPromise }).then(({ proxyWin, windowUrl, body, method }) => { return proxyWin.setLocation(windowUrl, { method, body }); }); const watchForClosePromise = openPromise.then(proxyWin => { watchForClose(proxyWin, context); }); const onDisplayPromise = ZalgoPromise.hash({ container: renderContainerPromise, prerender: prerenderPromise }).then(() => { return event.trigger(EVENT.DISPLAY); }); const openBridgePromise = openPromise.then(proxyWin => { return openBridge(proxyWin, initialChildDomain, context); }); const runTimeoutPromise = loadUrlPromise.then(() => { return runTimeout(); }); const onRenderedPromise = initPromise.then(() => { return event.trigger(EVENT.RENDERED); }); return ZalgoPromise.hash({ initPromise, buildUrlPromise, onRenderPromise, getProxyContainerPromise, openFramePromise, openPrerenderFramePromise, renderContainerPromise, openPromise, openPrerenderPromise, setStatePromise, prerenderPromise, loadUrlPromise, buildWindowNamePromise, setWindowNamePromise, watchForClosePromise, onDisplayPromise, openBridgePromise, runTimeoutPromise, onRenderedPromise, delegatePromise, watchForUnloadPromise, finalSetPropsPromise }); }).catch(err => { return ZalgoPromise.all([ onError(err), destroy(err) ]).then(() => { throw err; }, () => { throw err; }); }).then(noop); }; return { init, render, destroy, getProps, setProps, export: xport, getHelpers, getDelegateOverrides, getExports }; }