-
Notifications
You must be signed in to change notification settings - Fork 2
/
web-view.model.ts
412 lines (383 loc) · 16.6 KB
/
web-view.model.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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
import { Dispatch, ReactNode, SetStateAction } from 'react';
/**
* Saved information used to recreate a tab.
*
* {@link TabLoader} loads this into {@link TabInfo}
* {@link TabSaver} saves {@link TabInfo} into this
*/
export type SavedTabInfo = {
/**
* Tab ID - a unique identifier that identifies this tab. If this tab is a WebView, this id will
* match the WebViewDefinition.id
*/
id: string;
/**
* Type of tab - indicates what kind of built-in tab this info represents
*/
tabType: string;
/**
* Data needed to load the tab
*/
data?: unknown;
};
/**
* Information that Paranext uses to create a tab in the dock layout.
*
* {@link TabLoader} loads {@link SavedTabInfo} into this
* {@link TabSaver} saves this into {@link SavedTabInfo}
*/
export type TabInfo = SavedTabInfo & {
/**
* Url of image to show on the title bar of the tab
*
* Defaults to Platform.Bible logo
*/
tabIconUrl?: string;
/**
* Text to show on the title bar of the tab
*/
tabTitle: string;
/**
* Content to show inside the tab.
*/
content: ReactNode;
/**
* (optional) Minimum width that the tab can become in CSS `px` units
*/
minWidth?: number;
/**
* (optional) Minimum height that the tab can become in CSS `px` units
*/
minHeight?: number;
};
/**
* Function that takes a {@link SavedTabInfo} and creates a Paranext tab out of it. Each type of tab
* must provide a {@link TabLoader}.
*
* For now all tab creators must do their own data type verification
*/
export type TabLoader = (savedTabInfo: SavedTabInfo) => TabInfo;
/**
* Function that takes a Paranext tab and creates a saved tab out of it. Each type of tab can
* provide a {@link TabSaver}. If they do not provide one, the properties added by `TabInfo` are
* stripped from TabInfo by `saveTabInfoBase` before saving (so it is just a {@link SavedTabInfo}).
*
* @param tabInfo the Paranext tab to save
*
* @returns The saved tab info for Paranext to persist. If `undefined`, does not save the tab
*/
export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo | undefined;
/** The type of code that defines a webview's content */
export enum WebViewContentType {
/**
* This webview is a React webview. It must specify its component by setting it to
* `globalThis.webViewComponent`
*/
React = 'react',
/** This webview is a raw HTML/JS/CSS webview. */
HTML = 'html',
/**
* This webview's content is fetched from the url specified (iframe `src` attribute). Note that
* webViews of this type cannot access the `papi` because they cannot be on the same origin as the
* parent window.
*/
URL = 'url',
}
/** What type a WebView is. Each WebView definition must have a unique type. */
export type WebViewType = string;
/** Id for a specific WebView. Each WebView has a unique id */
export type WebViewId = string;
/** Base WebView properties that all WebViews share */
type WebViewDefinitionBase = {
/** What type of WebView this is. Unique to all other WebView definitions */
webViewType: WebViewType;
/** Unique id among webviews specific to this webview instance. */
id: WebViewId;
/** The code for the WebView that papi puts into an iframe */
content: string;
/**
* Url of image to show on the title bar of the tab
*
* Defaults to Platform.Bible logo
*/
iconUrl?: string;
/** Name of the tab for the WebView */
title?: string;
/** General object to store unique state for this webview */
state?: Record<string, unknown>;
/**
* Whether to allow the WebView iframe to interact with its parent as a same-origin website.
* Setting this to true adds `allow-same-origin` to the WebView iframe's [sandbox attribute]
* (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to `true`.
*
* Setting this to false on an HTML or React WebView prevents the iframe from importing the `papi`
* and such and also prevents others from accessing its document. This could be useful when you
* need secure input from the user because other WebViews may be able to attach event listeners to
* your inputs if you are on the same origin. Setting this to `false` on HTML or React WebViews
* is a big security win, but it makes interacting with the platform more challenging in some
* ways.
*
* Setting this to false on a URL WebView prevents the iframe from accessing same-origin features
* on its host website like storage APIs (localstorage, cookies, etc) and such. This will likely
* break many websites.
*
* It is best practice to set this to `false` where possible.
*
* Note: Until we have a message-passing APi for WebViews, there is currently no way to
* interact with the platform via a WebView with `allowSameOrigin: false`.
*
* WARNING: If your WebView accepts secure user input like passwords, you MUST set this to `false`
* or you will risk exposing that secure input to other extensions who could be phishing for it.
*/
allowSameOrigin?: boolean;
/**
* Whether to allow scripts to run in this iframe. Setting this to true adds `allow-scripts` to
* the WebView iframe's [sandbox attribute]
* (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox). Defaults to `true`
* for HTML and React WebViews and `false` for URL WebViews
*
* WARNING: Setting this to `true` increases the possibility of a security threat occurring. If it
* is not necessary to run scripts in your WebView, you should set this to `false` to reduce risk.
*/
// This does not follow our normal pattern of naming booleans because it mirrors the
// `allow-scripts` iframe sandbox attribute value
allowScripts?: boolean;
/**
* **For HTML and React WebViews:** List of [Host or scheme values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#hosts_values)
* to include in the [`frame-src` directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src)
* for this WebView's iframe content-security-policy. This allows iframes with `src` attributes
* matching these host values to be loaded in your WebView. You can only specify values starting
* with `papi-extension:` and `https:`; any others are ignored. Specifying urls in this array
* whitelists those urls so you can embed iframes with those urls as the `src` attribute. By
* default, no urls are available to be iframes. If you want to embed iframes with the `src`
* attribute in your webview, you must include them in this property.
*
* For example, if you specify `allowFrameSources: ['https://example.com/']`, you will be able to
* embed iframes with urls starting with `papi-extension:` and on the same host as `https://example.com/`
*
* If you plan on embedding any iframes in your WebView, it is best practice to list only the host
* values you need to function. The more you list, the higher the theoretical security risks.
*
* ----
*
* **For URL WebViews:** List of strings representing RegExp patterns (passed into
* [the RegExp constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp))
* to match against the `content` url specified (using the
* [`test`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)
* function) to determine whether this iframe will be allowed to load. Specifying urls in this
* array is essentially a security check to make sure the url you pass is one of the urls you
* intend it to be. By default, the url you specify in `content` will be accepted (you do not have
* to specify this unless you want to, but it is recommended in some scenarios).
*
* Note: URL WebViews must have `papi-extension:` or `https:` urls. This property does not
* override that requirement.
*
* For example, if you specify
* `allowFrameSources: ['^papi-extension:', '^https://example\\.com.*']`, only `papi-extension:` and
* `https://example.com` urls will be accepted.
*
* If your WebView url is a `const` string and cannot change for any reason, you do not need to
* specify this property. However, if your WebView url is dynamic and can change in any way, it is
* best practice to specify this property and to list only the urls you need for your URL WebView
* to function. The more you list, the higher the theoretical security risks.
*/
allowedFrameSources?: string[];
};
/** WebView representation using React */
export type WebViewDefinitionReact = WebViewDefinitionBase & {
/** Indicates this WebView uses React */
contentType?: WebViewContentType.React;
/** String of styles to be loaded into the iframe for this WebView */
styles?: string;
};
/** WebView representation using HTML */
export type WebViewDefinitionHtml = WebViewDefinitionBase & {
/** Indicates this WebView uses HTML */
contentType: WebViewContentType.HTML;
};
/**
* WebView representation using a URL.
*
* Note: you can only use `papi-extension:` and `https:` urls
*/
export type WebViewDefinitionURL = WebViewDefinitionBase & {
/** Indicates this WebView uses a URL */
contentType: WebViewContentType.URL;
};
/** Properties defining a type of WebView created by extensions to show web content */
export type WebViewDefinition =
| WebViewDefinitionReact
| WebViewDefinitionHtml
| WebViewDefinitionURL;
/**
* Saved WebView information that does not contain the actual content of the WebView. Saved into
* layouts. Could have as little as the type and id. WebView providers load these into actual
* {@link WebViewDefinition}s and verify any existing properties on the WebViews.
*/
export type SavedWebViewDefinition =
| (
| Partial<Omit<WebViewDefinitionReact, 'content' | 'styles' | 'allowScripts'>>
| Partial<Omit<WebViewDefinitionHtml, 'content' | 'allowScripts'>>
| Partial<Omit<WebViewDefinitionURL, 'content' | 'allowScripts'>>
) &
Pick<WebViewDefinitionBase, 'id' | 'webViewType'>;
/** Props that are passed to the web view tab component */
export type WebViewTabProps = WebViewDefinition;
/**
* The properties on a WebViewDefinition that may be updated when that webview is already displayed
*/
// To allow more properties to be updated, add them in
// `web-view.service.ts` -> `getUpdatablePropertiesFromWebViewDefinition`
export type WebViewDefinitionUpdatableProperties = Pick<WebViewDefinitionBase, 'iconUrl' | 'title'>;
/**
* WebViewDefinition properties for updating a WebView that is already displayed. Any unspecified
* properties will stay the same
*/
// To allow more properties to be updated, add them in
// `web-view.service.ts` -> `getUpdatablePropertiesFromWebViewDefinition`
export type WebViewDefinitionUpdateInfo = Partial<WebViewDefinitionUpdatableProperties>;
// This hook is found in `use-webview-state.hook.ts`
// Note: the following comment uses @, not the actual @ character, to hackily provide @param and
// such on this type. It seem that, for some reason, JSDoc does not carry these annotations on
// destructured members of object types, so using WebViewProps as
// `{ useWebViewState }: WebViewProps` was not carrying the annotations out to the new
// `useWebViewState` variable. One day, this may work, so we can fix this JSDoc back to using real @
/** JSDOC SOURCE UseWebViewStateHook
* A React hook for working with a state object tied to a webview. Returns a WebView state value and
* a function to set it. Use similarly to `useState`.
*
* Only used in WebView iframes.
*
* *@param* `stateKey` Key of the state value to use. The webview state holds a unique value per
* key.
*
* NOTE: `stateKey` needs to be a constant string, not something that could change during execution.
*
* *@param* `defaultStateValue` Value to use if the web view state didn't contain a value for the
* given 'stateKey'
*
* *@returns* `[stateValue, setStateValue]`
* - `stateValue`: the current value for the web view state at the key specified or
* `defaultStateValue` if a state was not found
* - `setStateValue`: function to use to update the web view state value at the key specified
*
* *@example*
* ```typescript
* const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen', 'No one');
* ```
*/
export type UseWebViewStateHook = <T>(
stateKey: string,
defaultStateValue: NonNullable<T>,
) => [webViewState: NonNullable<T>, setWebViewState: Dispatch<SetStateAction<NonNullable<T>>>];
// Note: the following comment uses @, not the actual @ character, to hackily provide @param and
// such on this type. It seem that, for some reason, JSDoc does not carry these annotations on
// destructured members of object types. See comment above UseWebViewStateHook for more info.
/** JSDOC SOURCE GetWebViewDefinitionUpdatableProperties
* Gets the updatable properties on this WebView's WebView definition
*
* *@returns* updatable properties this WebView's WebView definition or undefined if not found for
* some reason
*/
export type GetWebViewDefinitionUpdatableProperties = () =>
| WebViewDefinitionUpdatableProperties
| undefined;
// Note: the following comment uses @, not the actual @ character, to hackily provide @param and
// such on this type. It seem that, for some reason, JSDoc does not carry these annotations on
// destructured members of object types. See comment above UseWebViewStateHook for more info.
/** JSDOC SOURCE UpdateWebViewDefinition
* Updates this WebView with the specified properties
*
* *@param* `updateInfo` properties to update on the WebView. Any unspecified
* properties will stay the same
*
* *@returns* true if successfully found the WebView to update; false otherwise
*
* *@example*
* ```typescript
* updateWebViewDefinition({ title: `Hello ${name}`});
* ```
*/
export type UpdateWebViewDefinition = (updateInfo: WebViewDefinitionUpdateInfo) => boolean;
/**
* Props that are passed into the web view itself inside the iframe in the web view tab component
*/
export type WebViewProps = {
/** JSDOC DESTINATION UseWebViewStateHook */
useWebViewState: UseWebViewStateHook;
/** JSDOC DESTINATION GetWebViewDefinitionUpdatableProperties */
getWebViewDefinitionUpdatableProperties: GetWebViewDefinitionUpdatableProperties;
/** JSDOC DESTINATION UpdateWebViewDefinition */
updateWebViewDefinition: UpdateWebViewDefinition;
};
/** Information about a tab in a panel */
interface TabLayout {
type: 'tab';
}
/**
* Indicates where to display a floating window
*
* `cascade` - place the window a bit below and to the right of the previously created floating
* window
* `center` - center the window in the dock layout
*/
type FloatPosition = 'cascade' | 'center';
/** The dimensions for a floating tab in CSS `px` units */
export type FloatSize = { width: number; height: number };
/** Information about a floating window */
export interface FloatLayout {
type: 'float';
floatSize?: FloatSize;
/** Where to display the floating window. Defaults to `cascade` */
position?: FloatPosition;
}
export type PanelDirection =
| 'left'
| 'right'
| 'bottom'
| 'top'
// TODO: The following produce a panel but probably aren't useful for panels - IJH 2023-05-5
| 'before-tab'
| 'after-tab'
| 'maximize'
| 'move'
| 'active'
| 'update';
/** Information about a panel */
interface PanelLayout {
type: 'panel';
direction?: PanelDirection;
/** If undefined, it will add in the `direction` relative to the previously added tab. */
targetTabId?: string;
}
/** Information about how a Paranext tab fits into the dock layout */
export type Layout = TabLayout | FloatLayout | PanelLayout;
/** Event emitted when webViews are created */
export type AddWebViewEvent = {
webView: SavedWebViewDefinition;
layout: Layout;
};
/** Options that affect what `webViews.getWebView` does */
export type GetWebViewOptions = {
/**
* If provided and if a web view with this id exists, requests from the web view provider an
* existing WebView with this id if one exists. The web view provider can deny the request if it
* chooses to do so.
*
* Alternatively, set this to '?' to attempt to find any existing web view with the specified
* webViewType.
*
* Note: setting `existingId` to `undefined` counts as providing in this case (providing is tested
* with `'existingId' in options`, not just testing if `existingId` is truthy). Not providing an
* `existingId` at all is the only way to specify we are not looking for an existing webView
*/
existingId?: string | '?' | undefined;
/**
* Whether to create a webview with a new id and a webview with id `existingId` was not found.
* Only relevant if `existingId` is provided. If `existingId` is not provided, this property is
* ignored.
*
* Defaults to true
*/
createNewIfNotFound?: boolean;
};