-
-
Notifications
You must be signed in to change notification settings - Fork 144
/
index.ts
130 lines (109 loc) Β· 3.73 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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;
private connected: boolean;
constructor(private readonly config: Config) {
this.buffer = [];
this.handler = null;
window.addEventListener('message', this.handleEvent.bind(this), false);
// 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 = (...args) => {
handler.apply(this, args);
if (!this.connected && this.getWindow()) {
this.flush();
this.connected = true;
}
};
}
/**
* 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, options?: any): Promise<any> {
const iframeWindow = this.getWindow();
if (!iframeWindow || this.buffer.length) {
return new Promise((resolve, reject) => {
this.buffer.push({ event, resolve, reject });
});
}
let depth = 15;
let allowFunction = true;
if (options && typeof options.allowFunction === 'boolean') {
allowFunction = options.allowFunction;
}
if (options && Number.isInteger(options.depth)) {
depth = options.depth;
}
const data = stringify({ key: KEY, event }, { maxDepth: depth, allowFunction });
// TODO: investigate http:https://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;
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 });
}