/
launchAsync.ts
276 lines (231 loc) · 11 KB
/
launchAsync.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { Content } from './content';
import { CookiePolicy, DisplayOptions, Options, ReadAloudOptions, TranslationOptions } from './options';
import { Error, ErrorCode } from './error';
import { LaunchResponse } from './launchResponse';
declare const VERSION: string;
type Message = {
cogSvcsAccessToken: string;
cogSvcsSubdomain: string;
request: Content;
launchToPostMessageSentDurationInMs: number;
disableFirstRun?: boolean;
readAloudOptions?: ReadAloudOptions;
translationOptions?: TranslationOptions;
displayOptions?: DisplayOptions;
sendPreferences?: boolean;
preferences?: string;
};
type LaunchResponseMessage = {
success: boolean;
errorCode?: ErrorCode;
sessionId: string;
meteredContentSize?: number;
};
const sdkPlatform = 'js';
const sdkVersion = VERSION;
const PostMessagePreferences = 'ImmersiveReader-Preferences:';
const PostMessageLaunchResponse = 'ImmersiveReader-LaunchResponse:';
const errorMessageMap: { [errorCode: string]: string } = {};
errorMessageMap[ErrorCode.TokenExpired] = 'The access token supplied is expired.';
errorMessageMap[ErrorCode.Throttled] = 'You have exceeded your quota.';
errorMessageMap[ErrorCode.ServerError] = 'An error occurred when calling the server to process the text.';
errorMessageMap[ErrorCode.InvalidSubdomain] = 'The subdomain supplied is invalid.';
let isLoading: boolean = false;
/**
* Launch the Immersive Reader within an iframe.
* @param token The authentication token.
* @param subdomain The Immersive Reader Cognitive Service subdomain. This is a required parameter for Azure AD authentication in this and future versions of this SDK. Use of the Cognitive Services issueToken endpoint-based authentication tokens is deprecated and no longer supported.
* @param content The content that should be shown in the Immersive Reader.
* @param options Options for configuring the look and feel of the Immersive Reader.
* @return A promise that resolves with a LaunchResponse when the Immersive Reader is launched.
*/
export function launchAsync(token: string, subdomain: string, content: Content, options?: Options): Promise<LaunchResponse> {
if (isLoading) {
return Promise.reject('Immersive Reader is already launching');
}
return new Promise((resolve, reject: (reason: Error) => void): void => {
if (!token) {
reject({ code: ErrorCode.BadArgument, message: 'Token must not be null' });
return;
}
if (!content) {
reject({ code: ErrorCode.BadArgument, message: 'Content must not be null' });
return;
}
if (!content.chunks) {
reject({ code: ErrorCode.BadArgument, message: 'Chunks must not be null' });
return;
}
if (!content.chunks.length) {
reject({ code: ErrorCode.BadArgument, message: 'Chunks must not be empty' });
return;
}
if (!isValidSubdomain(subdomain) && (!options || !options.customDomain)) {
reject({ code: ErrorCode.InvalidSubdomain, message: errorMessageMap[ErrorCode.InvalidSubdomain] });
return;
}
isLoading = true;
const startTime = Date.now();
options = {
uiZIndex: 1000,
timeout: 15000, // Default to 15 seconds
useWebview: false,
allowFullscreen: true,
hideExitButton: false,
cookiePolicy: CookiePolicy.Disable,
...options
};
// Ensure that we were given a number for the UI z-index
if (!options.uiZIndex || typeof options.uiZIndex !== 'number') {
options.uiZIndex = 1000;
}
let timeoutId: number | null = null;
const iframeContainer: HTMLDivElement = document.createElement('div');
const iframe: HTMLIFrameElement = options.useWebview ? <HTMLIFrameElement>document.createElement('webview') : document.createElement('iframe');
iframe.allow = 'autoplay';
const noscroll: HTMLStyleElement = document.createElement('style');
noscroll.innerHTML = 'body{height:100%;overflow:hidden;}';
const resetTimeout = (): void => {
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
};
const parent = options.parent ? options.parent : document.body;
const reset = (): void => {
// Remove container along with the iframe inside of it
if (parent.contains(iframeContainer)) {
parent.removeChild(iframeContainer);
}
window.removeEventListener('message', messageHandler);
// Clear the timeout timer
resetTimeout();
// Re-enable scrolling
if (noscroll.parentNode) {
noscroll.parentNode.removeChild(noscroll);
}
};
const exit = (): void => {
reset();
// Execute exit callback if we have one
if (options.onExit) {
try {
options.onExit();
} catch { }
}
};
// Reset variables
reset();
const messageHandler = (e: any): void => {
// Don't process the message if the data is not a string
if (!e || !e.data || typeof e.data !== 'string') { return; }
if (e.data === 'ImmersiveReader-ReadyForContent') {
resetTimeout(); // Reset the timeout once the reader page loads successfully. The Reader page will report further errors through PostMessage if there is an issue obtaining the ContentModel from the server
const message: Message = {
cogSvcsAccessToken: token,
cogSvcsSubdomain: subdomain,
request: content,
launchToPostMessageSentDurationInMs: Date.now() - startTime,
disableFirstRun: options.disableFirstRun,
readAloudOptions: options.readAloudOptions,
translationOptions: options.translationOptions,
displayOptions: options.displayOptions,
sendPreferences: !!options.onPreferencesChanged,
preferences: options.preferences
};
iframe.contentWindow!.postMessage(JSON.stringify({ messageType: 'Content', messageValue: message }), '*');
} else if (e.data === 'ImmersiveReader-Exit') {
exit();
} else if (e.data.startsWith(PostMessageLaunchResponse)) {
let launchResponse: LaunchResponse = null;
let error: Error = null;
let response: LaunchResponseMessage = null;
try {
response = JSON.parse(e.data.substring(PostMessageLaunchResponse.length));
} catch {
// No-op
}
if (response && response.success) {
launchResponse = {
container: iframeContainer,
sessionId: response.sessionId,
charactersProcessed: response.meteredContentSize
};
} else if (response && !response.success) {
error = {
code: response.errorCode,
message: errorMessageMap[response.errorCode],
sessionId: response.sessionId
};
} else {
error = {
code: ErrorCode.ServerError,
message: errorMessageMap[ErrorCode.ServerError]
};
}
isLoading = false;
if (launchResponse) {
resetTimeout();
resolve(launchResponse);
} else if (error) {
exit();
reject(error);
}
} else if (e.data.startsWith(PostMessagePreferences)) {
if (options.onPreferencesChanged && typeof options.onPreferencesChanged === 'function') {
try {
options.onPreferencesChanged(e.data.substring(PostMessagePreferences.length));
} catch { }
}
}
};
window.addEventListener('message', messageHandler);
// Reject the promise if the Immersive Reader page fails to load.
timeoutId = window.setTimeout((): void => {
reset();
isLoading = false;
reject({ code: ErrorCode.Timeout, message: `Page failed to load after timeout (${options.timeout} ms)` });
}, options.timeout);
// Create and style iframe
if (options.allowFullscreen) {
iframe.setAttribute('allowfullscreen', '');
}
iframe.style.cssText = options.parent ? 'position: static; width: 100%; height: 100%; left: 0; top: 0; border-width: 0' : 'position: static; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0';
// Send an initial message to the webview so it has a reference to this parent window
if (options.useWebview) {
iframe.addEventListener('loadstop', () => {
iframe.contentWindow.postMessage(JSON.stringify({ messageType: 'WebviewHost' }), '*');
});
}
const domain = options.customDomain ? options.customDomain : `https://${subdomain}.cognitiveservices.azure.com/immersivereader/webapp/v1.0/`;
let src = domain + 'reader?exitCallback=ImmersiveReader-Exit&sdkPlatform=' + sdkPlatform + '&sdkVersion=' + sdkVersion;
src += '&cookiePolicy=' + ((options.cookiePolicy === CookiePolicy.Enable) ? 'enable' : 'disable');
if (options.hideExitButton) {
src += '&hideExitButton=true';
}
if (options.uiLang) {
src += '&omkt=' + options.uiLang;
}
iframe.src = src;
iframeContainer.style.cssText = options.parent ? `position: relative; width: 100%; height: 100%; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden` : `position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; border-width: 0; -webkit-perspective: 1px; z-index: ${options.uiZIndex}; background: white; overflow: hidden`;
iframeContainer.appendChild(iframe);
parent.appendChild(iframeContainer);
// Disable body scrolling
document.head.appendChild(noscroll);
});
}
export function close(): void {
window.postMessage('ImmersiveReader-Exit', '*');
}
// The subdomain must be alphanumeric, and may contain '-',
// as long as the '-' does not start or end the subdomain.
export function isValidSubdomain(subdomain: string): boolean {
if (!subdomain) {
return false;
}
const validRegex = '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])$';
const regExp = new RegExp(validRegex);
return regExp.test(subdomain);
}