@opensumi/ide-webview 2.21.13 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/browser/abstract-webview.js +3 -3
  2. package/lib/browser/abstract-webview.js.map +1 -1
  3. package/lib/browser/contribution.js.map +1 -1
  4. package/lib/browser/electron-webview-webview.js.map +1 -1
  5. package/lib/browser/iframe-webview.js.map +1 -1
  6. package/lib/browser/index.js.map +1 -1
  7. package/lib/browser/plain-webview.js +3 -3
  8. package/lib/browser/plain-webview.js.map +1 -1
  9. package/lib/browser/webview-window.js.map +1 -1
  10. package/lib/browser/webview.service.d.ts +1 -1
  11. package/lib/browser/webview.service.d.ts.map +1 -1
  12. package/lib/browser/webview.service.js +38 -38
  13. package/lib/browser/webview.service.js.map +1 -1
  14. package/lib/electron-main/index.js.map +1 -1
  15. package/package.json +13 -12
  16. package/src/browser/abstract-webview.ts +231 -0
  17. package/src/browser/contribution.ts +80 -0
  18. package/src/browser/editor-webview.tsx +207 -0
  19. package/src/browser/electron-webview-webview.ts +156 -0
  20. package/src/browser/iframe-webview.ts +140 -0
  21. package/src/browser/index.ts +19 -0
  22. package/src/browser/plain-webview.ts +284 -0
  23. package/src/browser/types.ts +274 -0
  24. package/src/browser/webview-window.ts +130 -0
  25. package/src/browser/webview.service.ts +484 -0
  26. package/src/common/index.ts +1 -0
  27. package/src/electron-main/index.ts +31 -0
  28. package/src/electron-webview/host-channel.ts +64 -0
  29. package/src/electron-webview/host-preload.js +2 -0
  30. package/src/electron-webview/plain-preload.js +55 -0
  31. package/src/index.ts +1 -0
  32. package/src/webview-host/common.ts +108 -0
  33. package/src/webview-host/web-preload.ts +78 -0
  34. package/src/webview-host/webview-manager.ts +368 -0
  35. package/src/webview-host/webview.html +11 -0
@@ -0,0 +1,2 @@
1
+ /* istanbul ignore file */
2
+ require('./host-channel');
@@ -0,0 +1,55 @@
1
+ /* istanbul ignore file */
2
+ const { ipcRenderer } = require('electron');
3
+
4
+ const argv = process.argv;
5
+
6
+ const urlParams = new URLSearchParams(decodeURIComponent(window.location.search));
7
+ window.id = Number(urlParams.get('windowId'));
8
+
9
+ let parentWindowWebContentsId;
10
+ let additionalEnv;
11
+
12
+ argv.forEach((arg) => {
13
+ if (arg.indexOf('--parentWindowWebContentsId=') === 0) {
14
+ parentWindowWebContentsId = parseInt(arg.substr('--parentWindowWebContentsId='.length), 10);
15
+ }
16
+ if (arg.indexOf('--additionalEnv=') === 0) {
17
+ additionalEnv = JSON.parse(arg.substr('--additionalEnv='.length));
18
+ }
19
+ });
20
+
21
+ let postMessage;
22
+
23
+ if (parentWindowWebContentsId) {
24
+ postMessage = (message) => {
25
+ ipcRenderer.sendTo(parentWindowWebContentsId, 'cross-window-webview-message', {
26
+ from: window.id,
27
+ message,
28
+ });
29
+ };
30
+ } else {
31
+ postMessage = (message) => {
32
+ ipcRenderer.sendToHost('webview-message', message);
33
+ };
34
+ }
35
+
36
+ if (additionalEnv) {
37
+ window.env = Object.assign({}, additionalEnv);
38
+ }
39
+
40
+ const parent = window.parent;
41
+
42
+ window.parent = new Proxy(window.parent, {
43
+ get: (target, p) => {
44
+ if (p === 'postMessage') {
45
+ return postMessage;
46
+ } else {
47
+ return Reflect.get(target, p);
48
+ }
49
+ },
50
+ });
51
+
52
+ ipcRenderer.on('webview-message', (e, data) => {
53
+ const messageEvent = new MessageEvent('message', { data, source: parent });
54
+ window.dispatchEvent(messageEvent);
55
+ });
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './browser';
@@ -0,0 +1,108 @@
1
+ export const defaultCss = `body {
2
+ background-color: var(--vscode-editor-background);
3
+ color: var(--vscode-editor-foreground);
4
+ font-family: var(--vscode-font-family);
5
+ font-weight: var(--vscode-font-weight);
6
+ font-size: var(--vscode-font-size);
7
+ margin: 0;
8
+ padding: 0 20px;
9
+ }
10
+
11
+ img {
12
+ max-width: 100%;
13
+ max-height: 100%;
14
+ }
15
+
16
+ a {
17
+ color: var(--vscode-textLink-foreground);
18
+ }
19
+
20
+ a:hover {
21
+ color: var(--vscode-textLink-activeForeground);
22
+ }
23
+
24
+ a:focus,
25
+ input:focus,
26
+ select:focus,
27
+ textarea:focus {
28
+ outline: 1px solid -webkit-focus-ring-color;
29
+ outline-offset: -1px;
30
+ }
31
+
32
+ code {
33
+ color: var(--vscode-textPreformat-foreground);
34
+ }
35
+
36
+ blockquote {
37
+ background: var(--vscode-textBlockQuote-background);
38
+ border-color: var(--vscode-textBlockQuote-border);
39
+ }
40
+
41
+ ::-webkit-scrollbar {
42
+ width: 10px;
43
+ height: 10px;
44
+ }
45
+
46
+ ::-webkit-scrollbar-thumb {
47
+ background-color: var(--vscode-scrollbarSlider-background);
48
+ }
49
+ ::-webkit-scrollbar-thumb:hover {
50
+ background-color: var(--vscode-scrollbarSlider-hoverBackground);
51
+ }
52
+ ::-webkit-scrollbar-thumb:active {
53
+ background-color: var(--vscode-scrollbarSlider-activeBackground);
54
+ }
55
+ ::-webkit-scrollbar-corner {
56
+ background: transparent;
57
+ }`;
58
+
59
+ export interface IWebviewChannel {
60
+ postMessage: (channel: string, data?: any) => void;
61
+ onMessage: (channel: string, handler: any) => void;
62
+ focusIframeOnCreate?: boolean;
63
+ ready?: Promise<void>;
64
+ onIframeLoaded?: (iframe: HTMLIFrameElement) => void;
65
+ fakeLoad: boolean;
66
+ onKeydown?: (event: KeyboardEvent) => void;
67
+ }
68
+
69
+ function addslashes(str) {
70
+ // eslint-disable-next-line no-control-regex
71
+ return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
72
+ }
73
+
74
+ export function getVsCodeApiScript(state) {
75
+ return `
76
+ const acquireVsCodeApi = (function() {
77
+ const originalPostMessage = window.parent.postMessage.bind(window.parent);
78
+ const targetOrigin = '*';
79
+ let acquired = false;
80
+
81
+ let state = ${state ? `JSON.parse("${addslashes(JSON.stringify(state))}")` : undefined};
82
+
83
+ return () => {
84
+ if (acquired) {
85
+ throw new Error('An instance of the VS Code API has already been acquired');
86
+ }
87
+ acquired = true;
88
+ return Object.freeze({
89
+ postMessage: function(msg) {
90
+ return originalPostMessage({ command: 'onmessage', data: msg }, targetOrigin);
91
+ },
92
+ setState: function(newState) {
93
+ state = newState;
94
+ originalPostMessage({ command: 'do-update-state', data: JSON.parse(JSON.stringify(newState)) }, targetOrigin);
95
+ return newState;
96
+ },
97
+ getState: function() {
98
+ return state;
99
+ }
100
+ });
101
+ };
102
+ })();
103
+ delete window.parent;
104
+ delete window.top;
105
+ delete window.frameElement;
106
+ window.acquireVsCodeApi = acquireVsCodeApi;
107
+ `;
108
+ }
@@ -0,0 +1,78 @@
1
+ /* istanbul ignore file */
2
+ import { IWebviewChannel } from './common';
3
+ import { WebviewPanelManager } from './webview-manager';
4
+
5
+ export class WebIframeChannel implements IWebviewChannel {
6
+ private handlers = new Map();
7
+ focusIframeOnCreate?: boolean | undefined;
8
+ ready?: Promise<void> | undefined;
9
+ fakeLoad = false;
10
+ private isInDevelopmentMode = false;
11
+ private id = document!.location!.search!.match(/\bid=([\w-]+)/)![1];
12
+
13
+ constructor() {
14
+ window.addEventListener('message', (e) => {
15
+ if (e.data && (e.data.command === 'onmessage' || e.data.command === 'do-update-state')) {
16
+ // Came from inner iframe
17
+ this.postMessage(e.data.command, e.data.data);
18
+ return;
19
+ }
20
+
21
+ const channel = e.data.channel;
22
+ const handler = this.handlers.get(channel);
23
+ if (handler) {
24
+ handler(e, e.data.data);
25
+ } else {
26
+ // eslint-disable-next-line no-console
27
+ console.log('no handler for ', e);
28
+ }
29
+ });
30
+
31
+ this.ready = new Promise<void>(async (resolve) => {
32
+ // TODO 等待service worker完成 未来资源使用service worker时需要加入
33
+ resolve();
34
+ });
35
+
36
+ this.onMessage('devtools-opened', () => {
37
+ this.isInDevelopmentMode = true;
38
+ });
39
+ }
40
+
41
+ get inDev() {
42
+ return this.isInDevelopmentMode;
43
+ }
44
+
45
+ postMessage(channel, data?) {
46
+ if (window.parent !== window) {
47
+ window.parent.postMessage({ target: this.id, channel, data }, '*');
48
+ }
49
+ }
50
+
51
+ onMessage(channel, handler) {
52
+ this.handlers.set(channel, handler);
53
+ }
54
+
55
+ onIframeLoaded(newFrame) {
56
+ // newFrame.contentWindow.onbeforeunload = () => {
57
+ // if (this.isInDevelopmentMode) { // Allow reloads while developing a webview
58
+ // this.postMessage('do-reload');
59
+ // return false;
60
+ // }
61
+ // // Block navigation when not in development mode
62
+ // console.log('prevented webview navigation');
63
+ // return false;
64
+ // };
65
+ }
66
+
67
+ onKeydown(event: KeyboardEvent) {
68
+ // 在浏览器上,需要阻止一些默认的keydown快捷键
69
+ if (event.key === 's' && (event.metaKey || event.ctrlKey)) {
70
+ // 阻止保存
71
+ event.preventDefault();
72
+ }
73
+ }
74
+ }
75
+
76
+ /* tslint:disable */
77
+ new WebviewPanelManager(new WebIframeChannel());
78
+ /* tslint:enable */
@@ -0,0 +1,368 @@
1
+ import { defaultCss, IWebviewChannel, getVsCodeApiScript } from './common';
2
+
3
+ export class WebviewPanelManager {
4
+ private activeTheme = 'default';
5
+ private styles: { [key: string]: string };
6
+ private isHandlingScroll = false;
7
+ private updateId = 0;
8
+ private firstLoad = true;
9
+ private loadTimeout;
10
+ private pendingMessages: any[] = [];
11
+ private initialScrollProgress: number;
12
+ private ID: string | undefined;
13
+
14
+ constructor(private channel: IWebviewChannel) {
15
+ document.addEventListener('DOMContentLoaded', this.init.bind(this));
16
+ }
17
+
18
+ private init() {
19
+ const idMatch = document.location.search.match(/\bid=([\w-]+)/);
20
+ this.ID = idMatch ? idMatch[1] : undefined;
21
+ if (!document.body) {
22
+ return;
23
+ }
24
+
25
+ this.channel.onMessage('styles', (_event, data) => {
26
+ this.styles = data.styles;
27
+ this.activeTheme = data.activeTheme;
28
+
29
+ const target = this.getActiveFrame();
30
+ if (!target) {
31
+ return;
32
+ }
33
+
34
+ if (target.contentDocument) {
35
+ this.applyStyles(target.contentDocument, target.contentDocument.body);
36
+ }
37
+ });
38
+
39
+ // propagate focus
40
+ this.channel.onMessage('focus', () => {
41
+ const target = this.getActiveFrame();
42
+ if (target && target.contentWindow) {
43
+ target.contentWindow.focus();
44
+ }
45
+ });
46
+
47
+ this.channel.onMessage('content', async (_event, data) => this.setContent(data));
48
+
49
+ this.channel.onMessage('message', (_event, data) => {
50
+ const pending = this.getPendingFrame();
51
+ if (!pending) {
52
+ const target = this.getActiveFrame();
53
+ if (target) {
54
+ target.contentWindow?.postMessage(data, '*');
55
+ return;
56
+ }
57
+ }
58
+ this.pendingMessages.push(data);
59
+ });
60
+
61
+ this.trackFocus({
62
+ onFocus: () => this.channel.postMessage('did-focus'),
63
+ onBlur: () => this.channel.postMessage('did-blur'),
64
+ });
65
+
66
+ this.channel.postMessage('webview-ready', {});
67
+ }
68
+
69
+ private async setContent(data) {
70
+ const currentUpdateId = ++this.updateId;
71
+ await this.channel.ready;
72
+ if (currentUpdateId !== this.updateId) {
73
+ return;
74
+ }
75
+
76
+ const options = data.options;
77
+ const newDocument = this.toContentHtml(data);
78
+
79
+ const frame = this.getActiveFrame();
80
+ const wasFirstLoad = this.firstLoad;
81
+ // keep current scrollY around and use later
82
+ let setInitialScrollPosition;
83
+ if (this.firstLoad) {
84
+ this.firstLoad = false;
85
+ setInitialScrollPosition = (body, window) => {
86
+ if (!isNaN(this.initialScrollProgress)) {
87
+ if (window.scrollY === 0) {
88
+ window.scroll(0, body.clientHeight * this.initialScrollProgress);
89
+ }
90
+ }
91
+ };
92
+ } else {
93
+ const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow?.scrollY : 0;
94
+ setInitialScrollPosition = (body, window) => {
95
+ if (window.scrollY === 0) {
96
+ window.scroll(0, scrollY);
97
+ }
98
+ };
99
+ }
100
+
101
+ // Clean up old pending frames and set current one as new one
102
+ const previousPendingFrame = this.getPendingFrame();
103
+ if (previousPendingFrame) {
104
+ previousPendingFrame.setAttribute('id', '');
105
+ document.body.removeChild(previousPendingFrame);
106
+ }
107
+ if (!wasFirstLoad) {
108
+ this.pendingMessages = [];
109
+ }
110
+
111
+ const newFrame = document.createElement('iframe');
112
+ newFrame.setAttribute('id', 'pending-frame');
113
+ newFrame.setAttribute('frameborder', '0');
114
+ newFrame.setAttribute('allow', 'autoplay; clipboard-read; clipboard-write;');
115
+
116
+ const sandboxRules = new Set(['allow-same-origin', 'allow-pointer-lock']);
117
+ if (options.allowScripts) {
118
+ sandboxRules.add('allow-scripts');
119
+ sandboxRules.add('allow-downloads');
120
+ }
121
+ if (options.allowForms) {
122
+ sandboxRules.add('allow-forms');
123
+ }
124
+ newFrame.setAttribute('sandbox', Array.from(sandboxRules).join(' '));
125
+ if (this.channel.fakeLoad) {
126
+ // 使用service-worker时候
127
+ newFrame.src = `./fake.html?id=${this.ID}`;
128
+ }
129
+ newFrame.style.cssText =
130
+ 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
131
+ document.body.appendChild(newFrame);
132
+
133
+ if (!this.channel.fakeLoad) {
134
+ // write new content onto iframe
135
+ newFrame.contentDocument?.open();
136
+ }
137
+
138
+ newFrame.contentWindow?.addEventListener('keydown', this.handleInnerKeydown.bind(this));
139
+
140
+ newFrame.contentWindow?.addEventListener('DOMContentLoaded', (e) => {
141
+ if (this.channel.fakeLoad) {
142
+ newFrame.contentDocument?.open();
143
+ newFrame.contentDocument?.write(newDocument);
144
+ newFrame.contentDocument?.close();
145
+ hookupOnLoadHandlers(newFrame);
146
+ }
147
+ const contentDocument: HTMLDocument | undefined = e.target ? (e.target as HTMLDocument) : undefined;
148
+ if (contentDocument) {
149
+ this.applyStyles(contentDocument, contentDocument.body);
150
+ }
151
+ });
152
+
153
+ const onLoad = (contentDocument, contentWindow) => {
154
+ if (contentDocument && contentDocument.body) {
155
+ // Workaround for https://github.com/Microsoft/vscode/issues/12865
156
+ // check new scrollY and reset if neccessary
157
+ setInitialScrollPosition(contentDocument.body, contentWindow);
158
+ }
159
+
160
+ const newFrame = this.getPendingFrame();
161
+ if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) {
162
+ const oldActiveFrame = this.getActiveFrame();
163
+ if (oldActiveFrame) {
164
+ document.body.removeChild(oldActiveFrame);
165
+ }
166
+ // Styles may have changed since we created the element. Make sure we re-style
167
+ this.applyStyles(newFrame.contentDocument, newFrame.contentDocument.body);
168
+ newFrame.setAttribute('id', 'active-frame');
169
+ newFrame.style.visibility = 'visible';
170
+ if (this.channel.focusIframeOnCreate) {
171
+ newFrame.contentWindow?.focus();
172
+ }
173
+
174
+ contentWindow.addEventListener('scroll', this.handleInnerScroll.bind(this));
175
+
176
+ this.pendingMessages.forEach((data) => {
177
+ contentWindow.postMessage(data, '*');
178
+ });
179
+ this.pendingMessages = [];
180
+ }
181
+ };
182
+
183
+ /**
184
+ * @param {HTMLIFrameElement} newFrame
185
+ */
186
+ const hookupOnLoadHandlers = (newFrame) => {
187
+ clearTimeout(this.loadTimeout);
188
+ this.loadTimeout = undefined;
189
+ this.loadTimeout = setTimeout(() => {
190
+ clearTimeout(this.loadTimeout);
191
+ this.loadTimeout = undefined;
192
+ onLoad(newFrame.contentDocument, newFrame.contentWindow);
193
+ }, 1000);
194
+
195
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
196
+ const _this = this;
197
+ newFrame.contentWindow.addEventListener('load', function (this: any, e) {
198
+ if (_this.loadTimeout) {
199
+ clearTimeout(_this.loadTimeout);
200
+ _this.loadTimeout = undefined;
201
+ onLoad(e.target, this);
202
+ }
203
+ });
204
+
205
+ // Bubble out link clicks
206
+ newFrame.contentWindow.addEventListener('click', this.handleInnerClick.bind(this));
207
+
208
+ if (this.channel.onIframeLoaded) {
209
+ this.channel.onIframeLoaded(newFrame);
210
+ }
211
+ };
212
+
213
+ if (!this.channel.fakeLoad) {
214
+ hookupOnLoadHandlers(newFrame);
215
+ }
216
+
217
+ if (!this.channel.fakeLoad) {
218
+ newFrame.contentDocument?.write(newDocument);
219
+ newFrame.contentDocument?.close();
220
+ }
221
+
222
+ this.channel.postMessage('did-set-content', undefined);
223
+ }
224
+
225
+ private trackFocus({ onFocus, onBlur }): void {
226
+ const interval = 50;
227
+ let isFocused = document.hasFocus();
228
+ setInterval(() => {
229
+ const isCurrentlyFocused = document.hasFocus();
230
+ if (isCurrentlyFocused === isFocused) {
231
+ return;
232
+ }
233
+ isFocused = isCurrentlyFocused;
234
+ if (isCurrentlyFocused) {
235
+ onFocus();
236
+ } else {
237
+ onBlur();
238
+ }
239
+ }, interval);
240
+ }
241
+
242
+ private getActiveFrame(): HTMLIFrameElement | undefined {
243
+ return document.getElementById('active-frame') as HTMLIFrameElement;
244
+ }
245
+
246
+ private getPendingFrame(): HTMLIFrameElement | undefined {
247
+ return document.getElementById('pending-frame') as HTMLIFrameElement;
248
+ }
249
+
250
+ private get defaultCssRules() {
251
+ return defaultCss;
252
+ }
253
+
254
+ private applyStyles(document, body) {
255
+ if (!document) {
256
+ return;
257
+ }
258
+
259
+ if (body) {
260
+ body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast');
261
+ body.classList.add(this.activeTheme);
262
+ }
263
+
264
+ if (this.styles) {
265
+ for (const variable of Object.keys(this.styles)) {
266
+ document.documentElement.style.setProperty(`--${variable}`, this.styles[variable]);
267
+ }
268
+ }
269
+ }
270
+
271
+ private handleInnerClick(event: MouseEvent) {
272
+ if (!event || !event.view || !event.view.document) {
273
+ return;
274
+ }
275
+
276
+ const baseElement = event.view.document.getElementsByTagName('base')[0];
277
+ let node = event.target as any;
278
+ while (node) {
279
+ if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) {
280
+ if (node.getAttribute('href') === '#') {
281
+ event.view.scrollTo(0, 0);
282
+ } else if (
283
+ node.hash &&
284
+ (node.getAttribute('href') === node.hash || (baseElement && node.href.indexOf(baseElement.href) >= 0))
285
+ ) {
286
+ const scrollTarget = event.view.document.getElementById(node.hash.substr(1, node.hash.length - 1));
287
+ if (scrollTarget) {
288
+ scrollTarget.scrollIntoView();
289
+ }
290
+ } else {
291
+ this.channel.postMessage('did-click-link', node.href.baseVal || node.href);
292
+ }
293
+ event.preventDefault();
294
+ break;
295
+ }
296
+ node = node.parentNode;
297
+ }
298
+ }
299
+
300
+ private handleInnerKeydown(e) {
301
+ this.channel.postMessage('did-keydown', {
302
+ key: e.key,
303
+ keyCode: e.keyCode,
304
+ code: e.code,
305
+ shiftKey: e.shiftKey,
306
+ altKey: e.altKey,
307
+ ctrlKey: e.ctrlKey,
308
+ metaKey: e.metaKey,
309
+ repeat: e.repeat,
310
+ });
311
+ this.channel.onKeydown && this.channel.onKeydown(e);
312
+ }
313
+
314
+ private handleInnerScroll(event) {
315
+ if (!event.target || !event.target.body) {
316
+ return;
317
+ }
318
+ if (this.isHandlingScroll) {
319
+ return;
320
+ }
321
+
322
+ const progress = event.currentTarget.scrollY / event.target.body.clientHeight;
323
+ if (isNaN(progress)) {
324
+ return;
325
+ }
326
+
327
+ this.isHandlingScroll = true;
328
+ window.requestAnimationFrame(() => {
329
+ try {
330
+ this.channel.postMessage('did-scroll', progress);
331
+ } catch (e) {
332
+ // noop
333
+ }
334
+ this.isHandlingScroll = false;
335
+ });
336
+ }
337
+
338
+ private toContentHtml(data) {
339
+ const options = data.options;
340
+ const text = data.contents;
341
+ const newDocument = new DOMParser().parseFromString(text, 'text/html');
342
+
343
+ newDocument.querySelectorAll('a').forEach((a) => {
344
+ if (!a.title) {
345
+ a.title = a.getAttribute('href')!;
346
+ }
347
+ });
348
+
349
+ // apply default script
350
+ if (options.allowScripts) {
351
+ const defaultScript = newDocument.createElement('script');
352
+ defaultScript.textContent = getVsCodeApiScript(data.state);
353
+ newDocument.head.prepend(defaultScript);
354
+ }
355
+
356
+ // apply default styles
357
+ const defaultStyles = newDocument.createElement('style');
358
+ defaultStyles.id = '_defaultStyles';
359
+ defaultStyles.innerHTML = this.defaultCssRules;
360
+ newDocument.head.prepend(defaultStyles);
361
+
362
+ this.applyStyles(newDocument, newDocument.body);
363
+
364
+ // set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off
365
+ // and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden
366
+ return '<!DOCTYPE html>\n' + newDocument.documentElement.outerHTML;
367
+ }
368
+ }
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" style="width: 100%; height: 100%">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta http-equiv="X-UA-Compatible" content="ie=edge" />
7
+ <title>Webview Panel Container</title>
8
+ </head>
9
+
10
+ <body style="margin: 0; overflow: hidden; width: 100%; height: 100%"></body>
11
+ </html>