mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 23:12:03 +08:00
112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
import { window, document } from 'global';
|
|
import Channel, { ChannelEvent, ChannelHandler } from '@storybook/channels';
|
|
import { logger } from '@storybook/client-logger';
|
|
|
|
import { isJSON, parse, stringify } from 'telejson';
|
|
|
|
interface RawEvent {
|
|
data: string;
|
|
}
|
|
|
|
interface Config {
|
|
page: 'manager' | 'preview';
|
|
}
|
|
|
|
interface BufferedEvent {
|
|
event: ChannelEvent;
|
|
resolve: (value?: any) => void;
|
|
reject: (reason?: any) => void;
|
|
}
|
|
|
|
export const KEY = 'storybook-channel';
|
|
|
|
// TODO: we should export a method for opening child windows here and keep track of em.
|
|
// that way we can send postMessage to child windows as well, not just iframe
|
|
// https://stackoverflow.com/questions/6340160/how-to-get-the-references-of-all-already-opened-child-windows
|
|
|
|
export class PostmsgTransport {
|
|
private buffer: BufferedEvent[];
|
|
private handler: ChannelHandler;
|
|
|
|
constructor(private readonly config: Config) {
|
|
this.buffer = [];
|
|
this.handler = null;
|
|
window.addEventListener('message', this.handleEvent.bind(this), false);
|
|
document.addEventListener('DOMContentLoaded', () => this.flush());
|
|
// Check whether the config.page parameter has a valid value
|
|
if (config.page !== 'manager' && config.page !== 'preview') {
|
|
throw new Error(`postmsg-channel: "config.page" cannot be "${config.page}"`);
|
|
}
|
|
}
|
|
|
|
setHandler(handler: ChannelHandler): void {
|
|
this.handler = handler;
|
|
}
|
|
|
|
/**
|
|
* Sends `event` to the associated window. If the window does not yet exist
|
|
* the event will be stored in a buffer and sent when the window exists.
|
|
* @param event
|
|
*/
|
|
send(event: ChannelEvent): Promise<any> {
|
|
const iframeWindow = this.getWindow();
|
|
if (!iframeWindow) {
|
|
return new Promise((resolve, reject) => {
|
|
this.buffer.push({ event, resolve, reject });
|
|
});
|
|
}
|
|
|
|
const data = stringify({ key: KEY, event }, { maxDepth: 10 });
|
|
|
|
// TODO: investigate http://blog.teamtreehouse.com/cross-domain-messaging-with-postmessage
|
|
// might replace '*' with document.location ?
|
|
iframeWindow.postMessage(data, '*');
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
private flush(): void {
|
|
const buffer = this.buffer;
|
|
this.buffer = [];
|
|
buffer.forEach(item => {
|
|
this.send(item.event)
|
|
.then(item.resolve)
|
|
.catch(item.reject);
|
|
});
|
|
}
|
|
|
|
private getWindow(): Window {
|
|
if (this.config.page === 'manager') {
|
|
// FIXME this is a really bad idea! use a better way to do this.
|
|
// This finds the storybook preview iframe to send messages to.
|
|
const iframe = document.getElementById('storybook-preview-iframe');
|
|
if (!iframe) {
|
|
return null;
|
|
}
|
|
return iframe.contentWindow;
|
|
}
|
|
return window.parent;
|
|
}
|
|
|
|
private handleEvent(rawEvent: RawEvent): void {
|
|
try {
|
|
const { data } = rawEvent;
|
|
const { key, event } = typeof data === 'string' && isJSON(data) ? parse(data) : data;
|
|
if (key === KEY) {
|
|
logger.debug(`message arrived at ${this.config.page}`, event.type, ...event.args);
|
|
this.handler(event);
|
|
}
|
|
} catch (error) {
|
|
logger.error(error);
|
|
// debugger;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a channel which communicates with an iframe or child window.
|
|
*/
|
|
export default function createChannel({ page }: Config): Channel {
|
|
const transport = new PostmsgTransport({ page });
|
|
return new Channel({ transport });
|
|
}
|