@theia/mini-browser 1.53.0-next.55 → 1.53.0-next.64
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.
- package/README.md +45 -45
- package/lib/browser/mini-browser-frontend-security-warnings.js +2 -2
- package/lib/node/mini-browser-backend-security-warnings.js +6 -6
- package/package.json +4 -4
- package/src/browser/environment/mini-browser-environment-module.ts +24 -24
- package/src/browser/environment/mini-browser-environment.ts +87 -87
- package/src/browser/location-mapper-service.ts +150 -150
- package/src/browser/mini-browser-content-style.ts +32 -32
- package/src/browser/mini-browser-content.ts +630 -630
- package/src/browser/mini-browser-frontend-module.ts +86 -86
- package/src/browser/mini-browser-frontend-security-warnings.ts +59 -59
- package/src/browser/mini-browser-open-handler.ts +312 -312
- package/src/browser/mini-browser.ts +110 -110
- package/src/browser/pdfobject.d.ts +99 -99
- package/src/browser/style/index.css +157 -157
- package/src/browser/style/mini-browser.svg +17 -17
- package/src/common/mini-browser-endpoint.ts +28 -28
- package/src/common/mini-browser-service.ts +29 -29
- package/src/electron-browser/environment/electron-mini-browser-environment-module.ts +25 -25
- package/src/electron-browser/environment/electron-mini-browser-environment.ts +53 -53
- package/src/electron-main/mini-browser-electron-main-contribution.ts +42 -42
- package/src/node/mini-browser-backend-module.ts +41 -41
- package/src/node/mini-browser-backend-security-warnings.ts +45 -45
- package/src/node/mini-browser-endpoint.ts +315 -315
- package/src/node/mini-browser-ws-validator.ts +56 -56
- package/src/package.spec.ts +21 -21
|
@@ -1,630 +1,630 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2018 TypeFox and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import * as PDFObject from 'pdfobject';
|
|
18
|
-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
19
|
-
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
|
20
|
-
import URI from '@theia/core/lib/common/uri';
|
|
21
|
-
import { ILogger } from '@theia/core/lib/common/logger';
|
|
22
|
-
import { Emitter } from '@theia/core/lib/common/event';
|
|
23
|
-
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
|
24
|
-
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
|
25
|
-
import { parseCssTime, Key, KeyCode } from '@theia/core/lib/browser';
|
|
26
|
-
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
|
27
|
-
import { BaseWidget, addEventListener, codiconArray } from '@theia/core/lib/browser/widgets/widget';
|
|
28
|
-
import { LocationMapperService } from './location-mapper-service';
|
|
29
|
-
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
|
|
30
|
-
|
|
31
|
-
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
32
|
-
import { MiniBrowserContentStyle } from './mini-browser-content-style';
|
|
33
|
-
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
34
|
-
import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Initializer properties for the embedded browser widget.
|
|
38
|
-
*/
|
|
39
|
-
@injectable()
|
|
40
|
-
export class MiniBrowserProps {
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `show` if the toolbar should be visible. If `read-only`, the toolbar is visible but the address cannot be changed and it acts as a link instead.\
|
|
44
|
-
* `hide` if the toolbar should be hidden. `show` by default. If the `startPage` is not defined, this property is always `show`.
|
|
45
|
-
*/
|
|
46
|
-
readonly toolbar?: 'show' | 'hide' | 'read-only';
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* If defined, the browser will load this page on startup. Otherwise, it show a blank page.
|
|
50
|
-
*/
|
|
51
|
-
readonly startPage?: string;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Sandbox options for the underlying `iframe`. Defaults to `SandboxOptions#DEFAULT` if not provided.
|
|
55
|
-
*/
|
|
56
|
-
readonly sandbox?: MiniBrowserProps.SandboxOptions[];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The optional icon class for the widget.
|
|
60
|
-
*/
|
|
61
|
-
readonly iconClass?: string;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The desired name of the widget.
|
|
65
|
-
*/
|
|
66
|
-
readonly name?: string;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* `true` if the `iFrame`'s background has to be reset to the default white color. Otherwise, `false`. `false` is the default.
|
|
70
|
-
*/
|
|
71
|
-
readonly resetBackground?: boolean;
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export namespace MiniBrowserProps {
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Enumeration of the supported `sandbox` options for the `iframe`.
|
|
79
|
-
*/
|
|
80
|
-
export enum SandboxOptions {
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Allows form submissions.
|
|
84
|
-
*/
|
|
85
|
-
'allow-forms',
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Allows popups, such as `window.open()`, `showModalDialog()`, `target=”_blank”`, etc.
|
|
89
|
-
*/
|
|
90
|
-
'allow-popups',
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Allows pointer lock.
|
|
94
|
-
*/
|
|
95
|
-
'allow-pointer-lock',
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Allows the document to maintain its origin. Pages loaded from https://example.com/ will retain access to that origin’s data.
|
|
99
|
-
*/
|
|
100
|
-
'allow-same-origin',
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Allows JavaScript execution. Also allows features to trigger automatically (as they’d be trivial to implement via JavaScript).
|
|
104
|
-
*/
|
|
105
|
-
'allow-scripts',
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Allows the document to break out of the frame by navigating the top-level `window`.
|
|
109
|
-
*/
|
|
110
|
-
'allow-top-navigation',
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Allows the embedded browsing context to open modal windows.
|
|
114
|
-
*/
|
|
115
|
-
'allow-modals',
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Allows the embedded browsing context to disable the ability to lock the screen orientation.
|
|
119
|
-
*/
|
|
120
|
-
'allow-orientation-lock',
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Allows a sandboxed document to open new windows without forcing the sandboxing flags upon them.
|
|
124
|
-
* This will allow, for example, a third-party advertisement to be safely sandboxed without forcing the same restrictions upon a landing page.
|
|
125
|
-
*/
|
|
126
|
-
'allow-popups-to-escape-sandbox',
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Allows embedders to have control over whether an iframe can start a presentation session.
|
|
130
|
-
*/
|
|
131
|
-
'allow-presentation',
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Allows the embedded browsing context to navigate (load) content to the top-level browsing context only when initiated by a user gesture.
|
|
135
|
-
* If this keyword is not used, this operation is not allowed.
|
|
136
|
-
*/
|
|
137
|
-
'allow-top-navigation-by-user-activation'
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export namespace SandboxOptions {
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* The default `sandbox` options, if other is not provided.
|
|
144
|
-
*
|
|
145
|
-
* See: https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
|
|
146
|
-
*/
|
|
147
|
-
export const DEFAULT: SandboxOptions[] = [
|
|
148
|
-
SandboxOptions['allow-same-origin'],
|
|
149
|
-
SandboxOptions['allow-scripts'],
|
|
150
|
-
SandboxOptions['allow-popups'],
|
|
151
|
-
SandboxOptions['allow-forms'],
|
|
152
|
-
SandboxOptions['allow-modals']
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export const MiniBrowserContentFactory = Symbol('MiniBrowserContentFactory');
|
|
160
|
-
export type MiniBrowserContentFactory = (props: MiniBrowserProps) => MiniBrowserContent;
|
|
161
|
-
|
|
162
|
-
@injectable()
|
|
163
|
-
export class MiniBrowserContent extends BaseWidget {
|
|
164
|
-
|
|
165
|
-
@inject(ILogger)
|
|
166
|
-
protected readonly logger: ILogger;
|
|
167
|
-
|
|
168
|
-
@inject(WindowService)
|
|
169
|
-
protected readonly windowService: WindowService;
|
|
170
|
-
|
|
171
|
-
@inject(LocationMapperService)
|
|
172
|
-
protected readonly locationMapper: LocationMapperService;
|
|
173
|
-
|
|
174
|
-
@inject(KeybindingRegistry)
|
|
175
|
-
protected readonly keybindings: KeybindingRegistry;
|
|
176
|
-
|
|
177
|
-
@inject(ApplicationShellMouseTracker)
|
|
178
|
-
protected readonly mouseTracker: ApplicationShellMouseTracker;
|
|
179
|
-
|
|
180
|
-
@inject(FileService)
|
|
181
|
-
protected readonly fileService: FileService;
|
|
182
|
-
|
|
183
|
-
protected readonly submitInputEmitter = new Emitter<string>();
|
|
184
|
-
protected readonly navigateBackEmitter = new Emitter<void>();
|
|
185
|
-
protected readonly navigateForwardEmitter = new Emitter<void>();
|
|
186
|
-
protected readonly refreshEmitter = new Emitter<void>();
|
|
187
|
-
protected readonly openEmitter = new Emitter<void>();
|
|
188
|
-
|
|
189
|
-
protected readonly input: HTMLInputElement;
|
|
190
|
-
protected readonly loadIndicator: HTMLElement;
|
|
191
|
-
protected readonly errorBar: HTMLElement & Readonly<{ message: HTMLElement }>;
|
|
192
|
-
protected readonly frame: HTMLIFrameElement;
|
|
193
|
-
// eslint-disable-next-line max-len
|
|
194
|
-
// XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets. On `mousedown` we put a transparent div over the `iframe` to avoid losing the mouse tacking.
|
|
195
|
-
protected readonly transparentOverlay: HTMLElement;
|
|
196
|
-
// XXX It is a hack. Instead of loading the PDF in an iframe we use `PDFObject` to render it in a div.
|
|
197
|
-
protected readonly pdfContainer: HTMLElement;
|
|
198
|
-
|
|
199
|
-
protected frameLoadTimeout: number;
|
|
200
|
-
protected readonly initialHistoryLength: number;
|
|
201
|
-
protected readonly toDisposeOnGo = new DisposableCollection();
|
|
202
|
-
|
|
203
|
-
constructor(@inject(MiniBrowserProps) protected readonly props: MiniBrowserProps) {
|
|
204
|
-
super();
|
|
205
|
-
this.node.tabIndex = 0;
|
|
206
|
-
this.addClass(MiniBrowserContentStyle.MINI_BROWSER);
|
|
207
|
-
this.input = this.createToolbar(this.node).input;
|
|
208
|
-
const contentArea = this.createContentArea(this.node);
|
|
209
|
-
this.frame = contentArea.frame;
|
|
210
|
-
this.transparentOverlay = contentArea.transparentOverlay;
|
|
211
|
-
this.loadIndicator = contentArea.loadIndicator;
|
|
212
|
-
this.errorBar = contentArea.errorBar;
|
|
213
|
-
this.pdfContainer = contentArea.pdfContainer;
|
|
214
|
-
this.initialHistoryLength = history.length;
|
|
215
|
-
this.toDispose.pushAll([
|
|
216
|
-
this.submitInputEmitter,
|
|
217
|
-
this.navigateBackEmitter,
|
|
218
|
-
this.navigateForwardEmitter,
|
|
219
|
-
this.refreshEmitter,
|
|
220
|
-
this.openEmitter
|
|
221
|
-
]);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
@postConstruct()
|
|
225
|
-
protected init(): void {
|
|
226
|
-
this.toDispose.push(this.mouseTracker.onMousedown(e => {
|
|
227
|
-
if (this.frame.style.display !== 'none') {
|
|
228
|
-
this.transparentOverlay.style.display = 'block';
|
|
229
|
-
}
|
|
230
|
-
}));
|
|
231
|
-
this.toDispose.push(this.mouseTracker.onMouseup(e => {
|
|
232
|
-
if (this.frame.style.display !== 'none') {
|
|
233
|
-
this.transparentOverlay.style.display = 'none';
|
|
234
|
-
}
|
|
235
|
-
}));
|
|
236
|
-
const { startPage } = this.props;
|
|
237
|
-
if (startPage) {
|
|
238
|
-
setTimeout(() => this.go(startPage), 500);
|
|
239
|
-
this.listenOnContentChange(startPage);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
protected override onActivateRequest(msg: Message): void {
|
|
244
|
-
super.onActivateRequest(msg);
|
|
245
|
-
if (this.getToolbarProps() !== 'hide') {
|
|
246
|
-
this.input.focus();
|
|
247
|
-
} else {
|
|
248
|
-
this.node.focus();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
protected async listenOnContentChange(location: string): Promise<void> {
|
|
253
|
-
if (await this.fileService.exists(new URI(location))) {
|
|
254
|
-
const fileUri = new URI(location);
|
|
255
|
-
const watcher = this.fileService.watch(fileUri);
|
|
256
|
-
this.toDispose.push(watcher);
|
|
257
|
-
const onFileChange = (event: FileChangesEvent) => {
|
|
258
|
-
if (event.contains(fileUri, FileChangeType.ADDED) || event.contains(fileUri, FileChangeType.UPDATED)) {
|
|
259
|
-
this.go(location, {
|
|
260
|
-
showLoadIndicator: false
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
this.toDispose.push(this.fileService.onDidFilesChange(debounce(onFileChange, 500)));
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
protected createToolbar(parent: HTMLElement): HTMLDivElement & Readonly<{ input: HTMLInputElement }> {
|
|
269
|
-
const toolbar = document.createElement('div');
|
|
270
|
-
toolbar.classList.add(this.getToolbarProps() === 'read-only' ? MiniBrowserContentStyle.TOOLBAR_READ_ONLY : MiniBrowserContentStyle.TOOLBAR);
|
|
271
|
-
parent.appendChild(toolbar);
|
|
272
|
-
this.createPrevious(toolbar);
|
|
273
|
-
this.createNext(toolbar);
|
|
274
|
-
this.createRefresh(toolbar);
|
|
275
|
-
const input = this.createInput(toolbar);
|
|
276
|
-
input.readOnly = this.getToolbarProps() === 'read-only';
|
|
277
|
-
this.createOpen(toolbar);
|
|
278
|
-
if (this.getToolbarProps() === 'hide') {
|
|
279
|
-
toolbar.style.display = 'none';
|
|
280
|
-
}
|
|
281
|
-
return Object.assign(toolbar, { input });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
protected getToolbarProps(): 'show' | 'hide' | 'read-only' {
|
|
285
|
-
return !this.props.startPage ? 'show' : this.props.toolbar || 'show';
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// eslint-disable-next-line max-len
|
|
289
|
-
protected createContentArea(parent: HTMLElement): HTMLElement & Readonly<{ frame: HTMLIFrameElement, loadIndicator: HTMLElement, errorBar: HTMLElement & Readonly<{ message: HTMLElement }>, pdfContainer: HTMLElement, transparentOverlay: HTMLElement }> {
|
|
290
|
-
const contentArea = document.createElement('div');
|
|
291
|
-
contentArea.classList.add(MiniBrowserContentStyle.CONTENT_AREA);
|
|
292
|
-
|
|
293
|
-
const loadIndicator = document.createElement('div');
|
|
294
|
-
loadIndicator.classList.add(MiniBrowserContentStyle.PRE_LOAD);
|
|
295
|
-
loadIndicator.style.display = 'none';
|
|
296
|
-
|
|
297
|
-
const errorBar = this.createErrorBar();
|
|
298
|
-
|
|
299
|
-
const frame = this.createIFrame();
|
|
300
|
-
this.submitInputEmitter.event(input => this.go(input, {
|
|
301
|
-
preserveFocus: false
|
|
302
|
-
}));
|
|
303
|
-
this.navigateBackEmitter.event(this.handleBack.bind(this));
|
|
304
|
-
this.navigateForwardEmitter.event(this.handleForward.bind(this));
|
|
305
|
-
this.refreshEmitter.event(this.handleRefresh.bind(this));
|
|
306
|
-
this.openEmitter.event(this.handleOpen.bind(this));
|
|
307
|
-
|
|
308
|
-
const transparentOverlay = document.createElement('div');
|
|
309
|
-
transparentOverlay.classList.add(MiniBrowserContentStyle.TRANSPARENT_OVERLAY);
|
|
310
|
-
transparentOverlay.style.display = 'none';
|
|
311
|
-
|
|
312
|
-
const pdfContainer = document.createElement('div');
|
|
313
|
-
pdfContainer.classList.add(MiniBrowserContentStyle.PDF_CONTAINER);
|
|
314
|
-
pdfContainer.id = `${this.id}-pdf-container`;
|
|
315
|
-
pdfContainer.style.display = 'none';
|
|
316
|
-
|
|
317
|
-
contentArea.appendChild(errorBar);
|
|
318
|
-
contentArea.appendChild(transparentOverlay);
|
|
319
|
-
contentArea.appendChild(pdfContainer);
|
|
320
|
-
contentArea.appendChild(loadIndicator);
|
|
321
|
-
contentArea.appendChild(frame);
|
|
322
|
-
|
|
323
|
-
parent.appendChild(contentArea);
|
|
324
|
-
return Object.assign(contentArea, { frame, loadIndicator, errorBar, pdfContainer, transparentOverlay });
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
protected createIFrame(): HTMLIFrameElement {
|
|
328
|
-
const frame = document.createElement('iframe');
|
|
329
|
-
const sandbox = (this.props.sandbox || MiniBrowserProps.SandboxOptions.DEFAULT).map(name => MiniBrowserProps.SandboxOptions[name]);
|
|
330
|
-
frame.sandbox.add(...sandbox);
|
|
331
|
-
this.toDispose.push(addEventListener(frame, 'load', this.onFrameLoad.bind(this)));
|
|
332
|
-
this.toDispose.push(addEventListener(frame, 'error', this.onFrameError.bind(this)));
|
|
333
|
-
return frame;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
protected createErrorBar(): HTMLElement & Readonly<{ message: HTMLElement }> {
|
|
337
|
-
const errorBar = document.createElement('div');
|
|
338
|
-
errorBar.classList.add(MiniBrowserContentStyle.ERROR_BAR);
|
|
339
|
-
errorBar.style.display = 'none';
|
|
340
|
-
|
|
341
|
-
const icon = document.createElement('span');
|
|
342
|
-
icon.classList.add(...codiconArray('info'));
|
|
343
|
-
errorBar.appendChild(icon);
|
|
344
|
-
|
|
345
|
-
const message = document.createElement('span');
|
|
346
|
-
errorBar.appendChild(message);
|
|
347
|
-
|
|
348
|
-
return Object.assign(errorBar, { message });
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
protected onFrameLoad(): void {
|
|
352
|
-
clearTimeout(this.frameLoadTimeout);
|
|
353
|
-
this.maybeResetBackground();
|
|
354
|
-
this.hideLoadIndicator();
|
|
355
|
-
this.hideErrorBar();
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
protected onFrameError(): void {
|
|
359
|
-
clearTimeout(this.frameLoadTimeout);
|
|
360
|
-
this.maybeResetBackground();
|
|
361
|
-
this.hideLoadIndicator();
|
|
362
|
-
this.showErrorBar('An error occurred while loading this page');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
protected onFrameTimeout(): void {
|
|
366
|
-
clearTimeout(this.frameLoadTimeout);
|
|
367
|
-
this.maybeResetBackground();
|
|
368
|
-
this.hideLoadIndicator();
|
|
369
|
-
this.showErrorBar('Still loading...');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
protected showLoadIndicator(): void {
|
|
373
|
-
this.loadIndicator.classList.remove(MiniBrowserContentStyle.FADE_OUT);
|
|
374
|
-
this.loadIndicator.style.display = 'block';
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
protected hideLoadIndicator(): void {
|
|
378
|
-
// Start the fade-out transition.
|
|
379
|
-
this.loadIndicator.classList.add(MiniBrowserContentStyle.FADE_OUT);
|
|
380
|
-
// Actually hide the load indicator after the transition is finished.
|
|
381
|
-
const preloadStyle = window.getComputedStyle(this.loadIndicator);
|
|
382
|
-
const transitionDuration = parseCssTime(preloadStyle.transitionDuration, 0);
|
|
383
|
-
setTimeout(() => {
|
|
384
|
-
// But don't hide it if it was shown again since the transition started.
|
|
385
|
-
if (this.loadIndicator.classList.contains(MiniBrowserContentStyle.FADE_OUT)) {
|
|
386
|
-
this.loadIndicator.style.display = 'none';
|
|
387
|
-
this.loadIndicator.classList.remove(MiniBrowserContentStyle.FADE_OUT);
|
|
388
|
-
}
|
|
389
|
-
}, transitionDuration);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
protected showErrorBar(message: string): void {
|
|
393
|
-
this.errorBar.message.textContent = message;
|
|
394
|
-
this.errorBar.style.display = 'block';
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
protected hideErrorBar(): void {
|
|
398
|
-
this.errorBar.message.textContent = '';
|
|
399
|
-
this.errorBar.style.display = 'none';
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
protected maybeResetBackground(): void {
|
|
403
|
-
if (this.props.resetBackground === true) {
|
|
404
|
-
this.frame.style.backgroundColor = 'white';
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
protected handleBack(): void {
|
|
409
|
-
if (history.length - this.initialHistoryLength > 0) {
|
|
410
|
-
history.back();
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
protected handleForward(): void {
|
|
415
|
-
if (history.length > this.initialHistoryLength) {
|
|
416
|
-
history.forward();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
protected handleRefresh(): void {
|
|
421
|
-
// Initial pessimism; use the location of the input.
|
|
422
|
-
let location: string | undefined = this.props.startPage;
|
|
423
|
-
// Use the the location from the `input`.
|
|
424
|
-
if (this.input && this.input.value) {
|
|
425
|
-
location = this.input.value;
|
|
426
|
-
}
|
|
427
|
-
try {
|
|
428
|
-
const { contentDocument } = this.frame;
|
|
429
|
-
if (contentDocument && contentDocument.location) {
|
|
430
|
-
location = contentDocument.location.href;
|
|
431
|
-
}
|
|
432
|
-
} catch {
|
|
433
|
-
// Security exception due to CORS when trying to access the `location.href` of the content document.
|
|
434
|
-
}
|
|
435
|
-
if (location) {
|
|
436
|
-
this.go(location, {
|
|
437
|
-
preserveFocus: false
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
protected handleOpen(): void {
|
|
443
|
-
const location = this.frameSrc() || this.input.value;
|
|
444
|
-
if (location) {
|
|
445
|
-
this.windowService.openNewWindow(location);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
protected createInput(parent: HTMLElement): HTMLInputElement {
|
|
450
|
-
const input = document.createElement('input');
|
|
451
|
-
input.type = 'text';
|
|
452
|
-
input.spellcheck = false;
|
|
453
|
-
input.classList.add('theia-input');
|
|
454
|
-
this.toDispose.pushAll([
|
|
455
|
-
addEventListener(input, 'keydown', this.handleInputChange.bind(this)),
|
|
456
|
-
addEventListener(input, 'click', () => {
|
|
457
|
-
if (this.getToolbarProps() === 'read-only') {
|
|
458
|
-
this.handleOpen();
|
|
459
|
-
} else {
|
|
460
|
-
if (input.value) {
|
|
461
|
-
input.select();
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
})
|
|
465
|
-
]);
|
|
466
|
-
parent.appendChild(input);
|
|
467
|
-
return input;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
protected handleInputChange(e: KeyboardEvent): void {
|
|
471
|
-
const { key } = KeyCode.createKeyCode(e);
|
|
472
|
-
if (key && Key.ENTER.keyCode === key.keyCode && this.getToolbarProps() === 'show') {
|
|
473
|
-
const { target } = e;
|
|
474
|
-
if (target instanceof HTMLInputElement) {
|
|
475
|
-
this.mapLocation(target.value).then(location => this.submitInputEmitter.fire(location));
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
protected createPrevious(parent: HTMLElement): HTMLElement {
|
|
481
|
-
return this.onClick(this.createButton(parent, 'Show The Previous Page', MiniBrowserContentStyle.PREVIOUS), this.navigateBackEmitter);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
protected createNext(parent: HTMLElement): HTMLElement {
|
|
485
|
-
return this.onClick(this.createButton(parent, 'Show The Next Page', MiniBrowserContentStyle.NEXT), this.navigateForwardEmitter);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
protected createRefresh(parent: HTMLElement): HTMLElement {
|
|
489
|
-
return this.onClick(this.createButton(parent, 'Reload This Page', MiniBrowserContentStyle.REFRESH), this.refreshEmitter);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
protected createOpen(parent: HTMLElement): HTMLElement {
|
|
493
|
-
const button = this.onClick(this.createButton(parent, 'Open In A New Window', MiniBrowserContentStyle.OPEN), this.openEmitter);
|
|
494
|
-
return button;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
protected createButton(parent: HTMLElement, title: string, ...className: string[]): HTMLElement {
|
|
498
|
-
const button = document.createElement('div');
|
|
499
|
-
button.title = title;
|
|
500
|
-
button.classList.add(...className, MiniBrowserContentStyle.BUTTON);
|
|
501
|
-
parent.appendChild(button);
|
|
502
|
-
return button;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
-
protected onClick(element: HTMLElement, emitter: Emitter<any>): HTMLElement {
|
|
507
|
-
this.toDispose.push(addEventListener(element, 'click', () => {
|
|
508
|
-
if (!element.classList.contains(MiniBrowserContentStyle.DISABLED)) {
|
|
509
|
-
emitter.fire(undefined);
|
|
510
|
-
}
|
|
511
|
-
}));
|
|
512
|
-
return element;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
protected mapLocation(location: string): Promise<string> {
|
|
516
|
-
return this.locationMapper.map(location);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
protected setInput(value: string): void {
|
|
520
|
-
if (this.input.value !== value) {
|
|
521
|
-
this.input.value = value;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
protected frameSrc(): string {
|
|
526
|
-
let src = this.frame.src;
|
|
527
|
-
try {
|
|
528
|
-
const { contentWindow } = this.frame;
|
|
529
|
-
if (contentWindow) {
|
|
530
|
-
src = contentWindow.location.href;
|
|
531
|
-
}
|
|
532
|
-
} catch {
|
|
533
|
-
// CORS issue. Ignored.
|
|
534
|
-
}
|
|
535
|
-
if (src === 'about:blank') {
|
|
536
|
-
src = '';
|
|
537
|
-
}
|
|
538
|
-
return src;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
protected contentDocument(): Document | null {
|
|
542
|
-
try {
|
|
543
|
-
let { contentDocument } = this.frame;
|
|
544
|
-
// eslint-disable-next-line no-null/no-null
|
|
545
|
-
if (contentDocument === null) {
|
|
546
|
-
const { contentWindow } = this.frame;
|
|
547
|
-
if (contentWindow) {
|
|
548
|
-
contentDocument = contentWindow.document;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return contentDocument;
|
|
552
|
-
} catch {
|
|
553
|
-
// eslint-disable-next-line no-null/no-null
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
protected async go(location: string, options?: Partial<{
|
|
559
|
-
/* default: true */
|
|
560
|
-
showLoadIndicator: boolean,
|
|
561
|
-
/* default: true */
|
|
562
|
-
preserveFocus: boolean
|
|
563
|
-
}>): Promise<void> {
|
|
564
|
-
const { showLoadIndicator, preserveFocus } = {
|
|
565
|
-
showLoadIndicator: true,
|
|
566
|
-
preserveFocus: true,
|
|
567
|
-
...options
|
|
568
|
-
};
|
|
569
|
-
if (location) {
|
|
570
|
-
try {
|
|
571
|
-
this.toDisposeOnGo.dispose();
|
|
572
|
-
const url = await this.mapLocation(location);
|
|
573
|
-
this.setInput(url);
|
|
574
|
-
if (this.getToolbarProps() === 'read-only') {
|
|
575
|
-
this.input.title = `Open ${url} In A New Window`;
|
|
576
|
-
}
|
|
577
|
-
clearTimeout(this.frameLoadTimeout);
|
|
578
|
-
this.frameLoadTimeout = window.setTimeout(this.onFrameTimeout.bind(this), 4000);
|
|
579
|
-
if (showLoadIndicator) {
|
|
580
|
-
this.showLoadIndicator();
|
|
581
|
-
}
|
|
582
|
-
if (url.endsWith('.pdf')) {
|
|
583
|
-
this.pdfContainer.style.display = 'block';
|
|
584
|
-
this.frame.style.display = 'none';
|
|
585
|
-
PDFObject.embed(url, this.pdfContainer, {
|
|
586
|
-
// eslint-disable-next-line max-len, @typescript-eslint/quotes
|
|
587
|
-
fallbackLink: `<p style="padding: 0px 15px 0px 15px">Your browser does not support inline PDFs. Click on this <a href='[url]' target="_blank">link</a> to open the PDF in a new tab.</p>`
|
|
588
|
-
});
|
|
589
|
-
clearTimeout(this.frameLoadTimeout);
|
|
590
|
-
this.hideLoadIndicator();
|
|
591
|
-
if (!preserveFocus) {
|
|
592
|
-
this.pdfContainer.focus();
|
|
593
|
-
}
|
|
594
|
-
} else {
|
|
595
|
-
this.pdfContainer.style.display = 'none';
|
|
596
|
-
this.frame.style.display = 'block';
|
|
597
|
-
this.frame.src = url;
|
|
598
|
-
// The load indicator will hide itself if the content of the iframe was loaded.
|
|
599
|
-
if (!preserveFocus) {
|
|
600
|
-
this.frame.addEventListener('load', () => {
|
|
601
|
-
const window = this.frame.contentWindow;
|
|
602
|
-
if (window) {
|
|
603
|
-
window.focus();
|
|
604
|
-
}
|
|
605
|
-
}, { once: true });
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
// Delegate all the `keypress` events from the `iframe` to the application.
|
|
609
|
-
this.toDisposeOnGo.push(addEventListener(this.frame, 'load', () => {
|
|
610
|
-
try {
|
|
611
|
-
const { contentDocument } = this.frame;
|
|
612
|
-
if (contentDocument) {
|
|
613
|
-
const keypressHandler = (e: KeyboardEvent) => this.keybindings.run(e);
|
|
614
|
-
contentDocument.addEventListener('keypress', keypressHandler, true);
|
|
615
|
-
this.toDisposeOnDetach.push(Disposable.create(() => contentDocument.removeEventListener('keypress', keypressHandler)));
|
|
616
|
-
}
|
|
617
|
-
} catch {
|
|
618
|
-
// There is not much we could do with the security exceptions due to CORS.
|
|
619
|
-
}
|
|
620
|
-
}));
|
|
621
|
-
} catch (e) {
|
|
622
|
-
clearTimeout(this.frameLoadTimeout);
|
|
623
|
-
this.hideLoadIndicator();
|
|
624
|
-
this.showErrorBar(String(e));
|
|
625
|
-
console.log(e);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2018 TypeFox and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as PDFObject from 'pdfobject';
|
|
18
|
+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
19
|
+
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
|
20
|
+
import URI from '@theia/core/lib/common/uri';
|
|
21
|
+
import { ILogger } from '@theia/core/lib/common/logger';
|
|
22
|
+
import { Emitter } from '@theia/core/lib/common/event';
|
|
23
|
+
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
|
24
|
+
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
|
25
|
+
import { parseCssTime, Key, KeyCode } from '@theia/core/lib/browser';
|
|
26
|
+
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
|
27
|
+
import { BaseWidget, addEventListener, codiconArray } from '@theia/core/lib/browser/widgets/widget';
|
|
28
|
+
import { LocationMapperService } from './location-mapper-service';
|
|
29
|
+
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
|
|
30
|
+
|
|
31
|
+
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
32
|
+
import { MiniBrowserContentStyle } from './mini-browser-content-style';
|
|
33
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
34
|
+
import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initializer properties for the embedded browser widget.
|
|
38
|
+
*/
|
|
39
|
+
@injectable()
|
|
40
|
+
export class MiniBrowserProps {
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `show` if the toolbar should be visible. If `read-only`, the toolbar is visible but the address cannot be changed and it acts as a link instead.\
|
|
44
|
+
* `hide` if the toolbar should be hidden. `show` by default. If the `startPage` is not defined, this property is always `show`.
|
|
45
|
+
*/
|
|
46
|
+
readonly toolbar?: 'show' | 'hide' | 'read-only';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* If defined, the browser will load this page on startup. Otherwise, it show a blank page.
|
|
50
|
+
*/
|
|
51
|
+
readonly startPage?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sandbox options for the underlying `iframe`. Defaults to `SandboxOptions#DEFAULT` if not provided.
|
|
55
|
+
*/
|
|
56
|
+
readonly sandbox?: MiniBrowserProps.SandboxOptions[];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The optional icon class for the widget.
|
|
60
|
+
*/
|
|
61
|
+
readonly iconClass?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The desired name of the widget.
|
|
65
|
+
*/
|
|
66
|
+
readonly name?: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `true` if the `iFrame`'s background has to be reset to the default white color. Otherwise, `false`. `false` is the default.
|
|
70
|
+
*/
|
|
71
|
+
readonly resetBackground?: boolean;
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export namespace MiniBrowserProps {
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enumeration of the supported `sandbox` options for the `iframe`.
|
|
79
|
+
*/
|
|
80
|
+
export enum SandboxOptions {
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Allows form submissions.
|
|
84
|
+
*/
|
|
85
|
+
'allow-forms',
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Allows popups, such as `window.open()`, `showModalDialog()`, `target=”_blank”`, etc.
|
|
89
|
+
*/
|
|
90
|
+
'allow-popups',
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Allows pointer lock.
|
|
94
|
+
*/
|
|
95
|
+
'allow-pointer-lock',
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Allows the document to maintain its origin. Pages loaded from https://example.com/ will retain access to that origin’s data.
|
|
99
|
+
*/
|
|
100
|
+
'allow-same-origin',
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Allows JavaScript execution. Also allows features to trigger automatically (as they’d be trivial to implement via JavaScript).
|
|
104
|
+
*/
|
|
105
|
+
'allow-scripts',
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Allows the document to break out of the frame by navigating the top-level `window`.
|
|
109
|
+
*/
|
|
110
|
+
'allow-top-navigation',
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Allows the embedded browsing context to open modal windows.
|
|
114
|
+
*/
|
|
115
|
+
'allow-modals',
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Allows the embedded browsing context to disable the ability to lock the screen orientation.
|
|
119
|
+
*/
|
|
120
|
+
'allow-orientation-lock',
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Allows a sandboxed document to open new windows without forcing the sandboxing flags upon them.
|
|
124
|
+
* This will allow, for example, a third-party advertisement to be safely sandboxed without forcing the same restrictions upon a landing page.
|
|
125
|
+
*/
|
|
126
|
+
'allow-popups-to-escape-sandbox',
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Allows embedders to have control over whether an iframe can start a presentation session.
|
|
130
|
+
*/
|
|
131
|
+
'allow-presentation',
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Allows the embedded browsing context to navigate (load) content to the top-level browsing context only when initiated by a user gesture.
|
|
135
|
+
* If this keyword is not used, this operation is not allowed.
|
|
136
|
+
*/
|
|
137
|
+
'allow-top-navigation-by-user-activation'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export namespace SandboxOptions {
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The default `sandbox` options, if other is not provided.
|
|
144
|
+
*
|
|
145
|
+
* See: https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
|
|
146
|
+
*/
|
|
147
|
+
export const DEFAULT: SandboxOptions[] = [
|
|
148
|
+
SandboxOptions['allow-same-origin'],
|
|
149
|
+
SandboxOptions['allow-scripts'],
|
|
150
|
+
SandboxOptions['allow-popups'],
|
|
151
|
+
SandboxOptions['allow-forms'],
|
|
152
|
+
SandboxOptions['allow-modals']
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const MiniBrowserContentFactory = Symbol('MiniBrowserContentFactory');
|
|
160
|
+
export type MiniBrowserContentFactory = (props: MiniBrowserProps) => MiniBrowserContent;
|
|
161
|
+
|
|
162
|
+
@injectable()
|
|
163
|
+
export class MiniBrowserContent extends BaseWidget {
|
|
164
|
+
|
|
165
|
+
@inject(ILogger)
|
|
166
|
+
protected readonly logger: ILogger;
|
|
167
|
+
|
|
168
|
+
@inject(WindowService)
|
|
169
|
+
protected readonly windowService: WindowService;
|
|
170
|
+
|
|
171
|
+
@inject(LocationMapperService)
|
|
172
|
+
protected readonly locationMapper: LocationMapperService;
|
|
173
|
+
|
|
174
|
+
@inject(KeybindingRegistry)
|
|
175
|
+
protected readonly keybindings: KeybindingRegistry;
|
|
176
|
+
|
|
177
|
+
@inject(ApplicationShellMouseTracker)
|
|
178
|
+
protected readonly mouseTracker: ApplicationShellMouseTracker;
|
|
179
|
+
|
|
180
|
+
@inject(FileService)
|
|
181
|
+
protected readonly fileService: FileService;
|
|
182
|
+
|
|
183
|
+
protected readonly submitInputEmitter = new Emitter<string>();
|
|
184
|
+
protected readonly navigateBackEmitter = new Emitter<void>();
|
|
185
|
+
protected readonly navigateForwardEmitter = new Emitter<void>();
|
|
186
|
+
protected readonly refreshEmitter = new Emitter<void>();
|
|
187
|
+
protected readonly openEmitter = new Emitter<void>();
|
|
188
|
+
|
|
189
|
+
protected readonly input: HTMLInputElement;
|
|
190
|
+
protected readonly loadIndicator: HTMLElement;
|
|
191
|
+
protected readonly errorBar: HTMLElement & Readonly<{ message: HTMLElement }>;
|
|
192
|
+
protected readonly frame: HTMLIFrameElement;
|
|
193
|
+
// eslint-disable-next-line max-len
|
|
194
|
+
// XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets. On `mousedown` we put a transparent div over the `iframe` to avoid losing the mouse tacking.
|
|
195
|
+
protected readonly transparentOverlay: HTMLElement;
|
|
196
|
+
// XXX It is a hack. Instead of loading the PDF in an iframe we use `PDFObject` to render it in a div.
|
|
197
|
+
protected readonly pdfContainer: HTMLElement;
|
|
198
|
+
|
|
199
|
+
protected frameLoadTimeout: number;
|
|
200
|
+
protected readonly initialHistoryLength: number;
|
|
201
|
+
protected readonly toDisposeOnGo = new DisposableCollection();
|
|
202
|
+
|
|
203
|
+
constructor(@inject(MiniBrowserProps) protected readonly props: MiniBrowserProps) {
|
|
204
|
+
super();
|
|
205
|
+
this.node.tabIndex = 0;
|
|
206
|
+
this.addClass(MiniBrowserContentStyle.MINI_BROWSER);
|
|
207
|
+
this.input = this.createToolbar(this.node).input;
|
|
208
|
+
const contentArea = this.createContentArea(this.node);
|
|
209
|
+
this.frame = contentArea.frame;
|
|
210
|
+
this.transparentOverlay = contentArea.transparentOverlay;
|
|
211
|
+
this.loadIndicator = contentArea.loadIndicator;
|
|
212
|
+
this.errorBar = contentArea.errorBar;
|
|
213
|
+
this.pdfContainer = contentArea.pdfContainer;
|
|
214
|
+
this.initialHistoryLength = history.length;
|
|
215
|
+
this.toDispose.pushAll([
|
|
216
|
+
this.submitInputEmitter,
|
|
217
|
+
this.navigateBackEmitter,
|
|
218
|
+
this.navigateForwardEmitter,
|
|
219
|
+
this.refreshEmitter,
|
|
220
|
+
this.openEmitter
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@postConstruct()
|
|
225
|
+
protected init(): void {
|
|
226
|
+
this.toDispose.push(this.mouseTracker.onMousedown(e => {
|
|
227
|
+
if (this.frame.style.display !== 'none') {
|
|
228
|
+
this.transparentOverlay.style.display = 'block';
|
|
229
|
+
}
|
|
230
|
+
}));
|
|
231
|
+
this.toDispose.push(this.mouseTracker.onMouseup(e => {
|
|
232
|
+
if (this.frame.style.display !== 'none') {
|
|
233
|
+
this.transparentOverlay.style.display = 'none';
|
|
234
|
+
}
|
|
235
|
+
}));
|
|
236
|
+
const { startPage } = this.props;
|
|
237
|
+
if (startPage) {
|
|
238
|
+
setTimeout(() => this.go(startPage), 500);
|
|
239
|
+
this.listenOnContentChange(startPage);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected override onActivateRequest(msg: Message): void {
|
|
244
|
+
super.onActivateRequest(msg);
|
|
245
|
+
if (this.getToolbarProps() !== 'hide') {
|
|
246
|
+
this.input.focus();
|
|
247
|
+
} else {
|
|
248
|
+
this.node.focus();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected async listenOnContentChange(location: string): Promise<void> {
|
|
253
|
+
if (await this.fileService.exists(new URI(location))) {
|
|
254
|
+
const fileUri = new URI(location);
|
|
255
|
+
const watcher = this.fileService.watch(fileUri);
|
|
256
|
+
this.toDispose.push(watcher);
|
|
257
|
+
const onFileChange = (event: FileChangesEvent) => {
|
|
258
|
+
if (event.contains(fileUri, FileChangeType.ADDED) || event.contains(fileUri, FileChangeType.UPDATED)) {
|
|
259
|
+
this.go(location, {
|
|
260
|
+
showLoadIndicator: false
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
this.toDispose.push(this.fileService.onDidFilesChange(debounce(onFileChange, 500)));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
protected createToolbar(parent: HTMLElement): HTMLDivElement & Readonly<{ input: HTMLInputElement }> {
|
|
269
|
+
const toolbar = document.createElement('div');
|
|
270
|
+
toolbar.classList.add(this.getToolbarProps() === 'read-only' ? MiniBrowserContentStyle.TOOLBAR_READ_ONLY : MiniBrowserContentStyle.TOOLBAR);
|
|
271
|
+
parent.appendChild(toolbar);
|
|
272
|
+
this.createPrevious(toolbar);
|
|
273
|
+
this.createNext(toolbar);
|
|
274
|
+
this.createRefresh(toolbar);
|
|
275
|
+
const input = this.createInput(toolbar);
|
|
276
|
+
input.readOnly = this.getToolbarProps() === 'read-only';
|
|
277
|
+
this.createOpen(toolbar);
|
|
278
|
+
if (this.getToolbarProps() === 'hide') {
|
|
279
|
+
toolbar.style.display = 'none';
|
|
280
|
+
}
|
|
281
|
+
return Object.assign(toolbar, { input });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
protected getToolbarProps(): 'show' | 'hide' | 'read-only' {
|
|
285
|
+
return !this.props.startPage ? 'show' : this.props.toolbar || 'show';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// eslint-disable-next-line max-len
|
|
289
|
+
protected createContentArea(parent: HTMLElement): HTMLElement & Readonly<{ frame: HTMLIFrameElement, loadIndicator: HTMLElement, errorBar: HTMLElement & Readonly<{ message: HTMLElement }>, pdfContainer: HTMLElement, transparentOverlay: HTMLElement }> {
|
|
290
|
+
const contentArea = document.createElement('div');
|
|
291
|
+
contentArea.classList.add(MiniBrowserContentStyle.CONTENT_AREA);
|
|
292
|
+
|
|
293
|
+
const loadIndicator = document.createElement('div');
|
|
294
|
+
loadIndicator.classList.add(MiniBrowserContentStyle.PRE_LOAD);
|
|
295
|
+
loadIndicator.style.display = 'none';
|
|
296
|
+
|
|
297
|
+
const errorBar = this.createErrorBar();
|
|
298
|
+
|
|
299
|
+
const frame = this.createIFrame();
|
|
300
|
+
this.submitInputEmitter.event(input => this.go(input, {
|
|
301
|
+
preserveFocus: false
|
|
302
|
+
}));
|
|
303
|
+
this.navigateBackEmitter.event(this.handleBack.bind(this));
|
|
304
|
+
this.navigateForwardEmitter.event(this.handleForward.bind(this));
|
|
305
|
+
this.refreshEmitter.event(this.handleRefresh.bind(this));
|
|
306
|
+
this.openEmitter.event(this.handleOpen.bind(this));
|
|
307
|
+
|
|
308
|
+
const transparentOverlay = document.createElement('div');
|
|
309
|
+
transparentOverlay.classList.add(MiniBrowserContentStyle.TRANSPARENT_OVERLAY);
|
|
310
|
+
transparentOverlay.style.display = 'none';
|
|
311
|
+
|
|
312
|
+
const pdfContainer = document.createElement('div');
|
|
313
|
+
pdfContainer.classList.add(MiniBrowserContentStyle.PDF_CONTAINER);
|
|
314
|
+
pdfContainer.id = `${this.id}-pdf-container`;
|
|
315
|
+
pdfContainer.style.display = 'none';
|
|
316
|
+
|
|
317
|
+
contentArea.appendChild(errorBar);
|
|
318
|
+
contentArea.appendChild(transparentOverlay);
|
|
319
|
+
contentArea.appendChild(pdfContainer);
|
|
320
|
+
contentArea.appendChild(loadIndicator);
|
|
321
|
+
contentArea.appendChild(frame);
|
|
322
|
+
|
|
323
|
+
parent.appendChild(contentArea);
|
|
324
|
+
return Object.assign(contentArea, { frame, loadIndicator, errorBar, pdfContainer, transparentOverlay });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
protected createIFrame(): HTMLIFrameElement {
|
|
328
|
+
const frame = document.createElement('iframe');
|
|
329
|
+
const sandbox = (this.props.sandbox || MiniBrowserProps.SandboxOptions.DEFAULT).map(name => MiniBrowserProps.SandboxOptions[name]);
|
|
330
|
+
frame.sandbox.add(...sandbox);
|
|
331
|
+
this.toDispose.push(addEventListener(frame, 'load', this.onFrameLoad.bind(this)));
|
|
332
|
+
this.toDispose.push(addEventListener(frame, 'error', this.onFrameError.bind(this)));
|
|
333
|
+
return frame;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
protected createErrorBar(): HTMLElement & Readonly<{ message: HTMLElement }> {
|
|
337
|
+
const errorBar = document.createElement('div');
|
|
338
|
+
errorBar.classList.add(MiniBrowserContentStyle.ERROR_BAR);
|
|
339
|
+
errorBar.style.display = 'none';
|
|
340
|
+
|
|
341
|
+
const icon = document.createElement('span');
|
|
342
|
+
icon.classList.add(...codiconArray('info'));
|
|
343
|
+
errorBar.appendChild(icon);
|
|
344
|
+
|
|
345
|
+
const message = document.createElement('span');
|
|
346
|
+
errorBar.appendChild(message);
|
|
347
|
+
|
|
348
|
+
return Object.assign(errorBar, { message });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
protected onFrameLoad(): void {
|
|
352
|
+
clearTimeout(this.frameLoadTimeout);
|
|
353
|
+
this.maybeResetBackground();
|
|
354
|
+
this.hideLoadIndicator();
|
|
355
|
+
this.hideErrorBar();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected onFrameError(): void {
|
|
359
|
+
clearTimeout(this.frameLoadTimeout);
|
|
360
|
+
this.maybeResetBackground();
|
|
361
|
+
this.hideLoadIndicator();
|
|
362
|
+
this.showErrorBar('An error occurred while loading this page');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
protected onFrameTimeout(): void {
|
|
366
|
+
clearTimeout(this.frameLoadTimeout);
|
|
367
|
+
this.maybeResetBackground();
|
|
368
|
+
this.hideLoadIndicator();
|
|
369
|
+
this.showErrorBar('Still loading...');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
protected showLoadIndicator(): void {
|
|
373
|
+
this.loadIndicator.classList.remove(MiniBrowserContentStyle.FADE_OUT);
|
|
374
|
+
this.loadIndicator.style.display = 'block';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
protected hideLoadIndicator(): void {
|
|
378
|
+
// Start the fade-out transition.
|
|
379
|
+
this.loadIndicator.classList.add(MiniBrowserContentStyle.FADE_OUT);
|
|
380
|
+
// Actually hide the load indicator after the transition is finished.
|
|
381
|
+
const preloadStyle = window.getComputedStyle(this.loadIndicator);
|
|
382
|
+
const transitionDuration = parseCssTime(preloadStyle.transitionDuration, 0);
|
|
383
|
+
setTimeout(() => {
|
|
384
|
+
// But don't hide it if it was shown again since the transition started.
|
|
385
|
+
if (this.loadIndicator.classList.contains(MiniBrowserContentStyle.FADE_OUT)) {
|
|
386
|
+
this.loadIndicator.style.display = 'none';
|
|
387
|
+
this.loadIndicator.classList.remove(MiniBrowserContentStyle.FADE_OUT);
|
|
388
|
+
}
|
|
389
|
+
}, transitionDuration);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
protected showErrorBar(message: string): void {
|
|
393
|
+
this.errorBar.message.textContent = message;
|
|
394
|
+
this.errorBar.style.display = 'block';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
protected hideErrorBar(): void {
|
|
398
|
+
this.errorBar.message.textContent = '';
|
|
399
|
+
this.errorBar.style.display = 'none';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
protected maybeResetBackground(): void {
|
|
403
|
+
if (this.props.resetBackground === true) {
|
|
404
|
+
this.frame.style.backgroundColor = 'white';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
protected handleBack(): void {
|
|
409
|
+
if (history.length - this.initialHistoryLength > 0) {
|
|
410
|
+
history.back();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
protected handleForward(): void {
|
|
415
|
+
if (history.length > this.initialHistoryLength) {
|
|
416
|
+
history.forward();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
protected handleRefresh(): void {
|
|
421
|
+
// Initial pessimism; use the location of the input.
|
|
422
|
+
let location: string | undefined = this.props.startPage;
|
|
423
|
+
// Use the the location from the `input`.
|
|
424
|
+
if (this.input && this.input.value) {
|
|
425
|
+
location = this.input.value;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const { contentDocument } = this.frame;
|
|
429
|
+
if (contentDocument && contentDocument.location) {
|
|
430
|
+
location = contentDocument.location.href;
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Security exception due to CORS when trying to access the `location.href` of the content document.
|
|
434
|
+
}
|
|
435
|
+
if (location) {
|
|
436
|
+
this.go(location, {
|
|
437
|
+
preserveFocus: false
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
protected handleOpen(): void {
|
|
443
|
+
const location = this.frameSrc() || this.input.value;
|
|
444
|
+
if (location) {
|
|
445
|
+
this.windowService.openNewWindow(location);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
protected createInput(parent: HTMLElement): HTMLInputElement {
|
|
450
|
+
const input = document.createElement('input');
|
|
451
|
+
input.type = 'text';
|
|
452
|
+
input.spellcheck = false;
|
|
453
|
+
input.classList.add('theia-input');
|
|
454
|
+
this.toDispose.pushAll([
|
|
455
|
+
addEventListener(input, 'keydown', this.handleInputChange.bind(this)),
|
|
456
|
+
addEventListener(input, 'click', () => {
|
|
457
|
+
if (this.getToolbarProps() === 'read-only') {
|
|
458
|
+
this.handleOpen();
|
|
459
|
+
} else {
|
|
460
|
+
if (input.value) {
|
|
461
|
+
input.select();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
]);
|
|
466
|
+
parent.appendChild(input);
|
|
467
|
+
return input;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
protected handleInputChange(e: KeyboardEvent): void {
|
|
471
|
+
const { key } = KeyCode.createKeyCode(e);
|
|
472
|
+
if (key && Key.ENTER.keyCode === key.keyCode && this.getToolbarProps() === 'show') {
|
|
473
|
+
const { target } = e;
|
|
474
|
+
if (target instanceof HTMLInputElement) {
|
|
475
|
+
this.mapLocation(target.value).then(location => this.submitInputEmitter.fire(location));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
protected createPrevious(parent: HTMLElement): HTMLElement {
|
|
481
|
+
return this.onClick(this.createButton(parent, 'Show The Previous Page', MiniBrowserContentStyle.PREVIOUS), this.navigateBackEmitter);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
protected createNext(parent: HTMLElement): HTMLElement {
|
|
485
|
+
return this.onClick(this.createButton(parent, 'Show The Next Page', MiniBrowserContentStyle.NEXT), this.navigateForwardEmitter);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
protected createRefresh(parent: HTMLElement): HTMLElement {
|
|
489
|
+
return this.onClick(this.createButton(parent, 'Reload This Page', MiniBrowserContentStyle.REFRESH), this.refreshEmitter);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
protected createOpen(parent: HTMLElement): HTMLElement {
|
|
493
|
+
const button = this.onClick(this.createButton(parent, 'Open In A New Window', MiniBrowserContentStyle.OPEN), this.openEmitter);
|
|
494
|
+
return button;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
protected createButton(parent: HTMLElement, title: string, ...className: string[]): HTMLElement {
|
|
498
|
+
const button = document.createElement('div');
|
|
499
|
+
button.title = title;
|
|
500
|
+
button.classList.add(...className, MiniBrowserContentStyle.BUTTON);
|
|
501
|
+
parent.appendChild(button);
|
|
502
|
+
return button;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
+
protected onClick(element: HTMLElement, emitter: Emitter<any>): HTMLElement {
|
|
507
|
+
this.toDispose.push(addEventListener(element, 'click', () => {
|
|
508
|
+
if (!element.classList.contains(MiniBrowserContentStyle.DISABLED)) {
|
|
509
|
+
emitter.fire(undefined);
|
|
510
|
+
}
|
|
511
|
+
}));
|
|
512
|
+
return element;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
protected mapLocation(location: string): Promise<string> {
|
|
516
|
+
return this.locationMapper.map(location);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
protected setInput(value: string): void {
|
|
520
|
+
if (this.input.value !== value) {
|
|
521
|
+
this.input.value = value;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
protected frameSrc(): string {
|
|
526
|
+
let src = this.frame.src;
|
|
527
|
+
try {
|
|
528
|
+
const { contentWindow } = this.frame;
|
|
529
|
+
if (contentWindow) {
|
|
530
|
+
src = contentWindow.location.href;
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
// CORS issue. Ignored.
|
|
534
|
+
}
|
|
535
|
+
if (src === 'about:blank') {
|
|
536
|
+
src = '';
|
|
537
|
+
}
|
|
538
|
+
return src;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
protected contentDocument(): Document | null {
|
|
542
|
+
try {
|
|
543
|
+
let { contentDocument } = this.frame;
|
|
544
|
+
// eslint-disable-next-line no-null/no-null
|
|
545
|
+
if (contentDocument === null) {
|
|
546
|
+
const { contentWindow } = this.frame;
|
|
547
|
+
if (contentWindow) {
|
|
548
|
+
contentDocument = contentWindow.document;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return contentDocument;
|
|
552
|
+
} catch {
|
|
553
|
+
// eslint-disable-next-line no-null/no-null
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
protected async go(location: string, options?: Partial<{
|
|
559
|
+
/* default: true */
|
|
560
|
+
showLoadIndicator: boolean,
|
|
561
|
+
/* default: true */
|
|
562
|
+
preserveFocus: boolean
|
|
563
|
+
}>): Promise<void> {
|
|
564
|
+
const { showLoadIndicator, preserveFocus } = {
|
|
565
|
+
showLoadIndicator: true,
|
|
566
|
+
preserveFocus: true,
|
|
567
|
+
...options
|
|
568
|
+
};
|
|
569
|
+
if (location) {
|
|
570
|
+
try {
|
|
571
|
+
this.toDisposeOnGo.dispose();
|
|
572
|
+
const url = await this.mapLocation(location);
|
|
573
|
+
this.setInput(url);
|
|
574
|
+
if (this.getToolbarProps() === 'read-only') {
|
|
575
|
+
this.input.title = `Open ${url} In A New Window`;
|
|
576
|
+
}
|
|
577
|
+
clearTimeout(this.frameLoadTimeout);
|
|
578
|
+
this.frameLoadTimeout = window.setTimeout(this.onFrameTimeout.bind(this), 4000);
|
|
579
|
+
if (showLoadIndicator) {
|
|
580
|
+
this.showLoadIndicator();
|
|
581
|
+
}
|
|
582
|
+
if (url.endsWith('.pdf')) {
|
|
583
|
+
this.pdfContainer.style.display = 'block';
|
|
584
|
+
this.frame.style.display = 'none';
|
|
585
|
+
PDFObject.embed(url, this.pdfContainer, {
|
|
586
|
+
// eslint-disable-next-line max-len, @typescript-eslint/quotes
|
|
587
|
+
fallbackLink: `<p style="padding: 0px 15px 0px 15px">Your browser does not support inline PDFs. Click on this <a href='[url]' target="_blank">link</a> to open the PDF in a new tab.</p>`
|
|
588
|
+
});
|
|
589
|
+
clearTimeout(this.frameLoadTimeout);
|
|
590
|
+
this.hideLoadIndicator();
|
|
591
|
+
if (!preserveFocus) {
|
|
592
|
+
this.pdfContainer.focus();
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
this.pdfContainer.style.display = 'none';
|
|
596
|
+
this.frame.style.display = 'block';
|
|
597
|
+
this.frame.src = url;
|
|
598
|
+
// The load indicator will hide itself if the content of the iframe was loaded.
|
|
599
|
+
if (!preserveFocus) {
|
|
600
|
+
this.frame.addEventListener('load', () => {
|
|
601
|
+
const window = this.frame.contentWindow;
|
|
602
|
+
if (window) {
|
|
603
|
+
window.focus();
|
|
604
|
+
}
|
|
605
|
+
}, { once: true });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Delegate all the `keypress` events from the `iframe` to the application.
|
|
609
|
+
this.toDisposeOnGo.push(addEventListener(this.frame, 'load', () => {
|
|
610
|
+
try {
|
|
611
|
+
const { contentDocument } = this.frame;
|
|
612
|
+
if (contentDocument) {
|
|
613
|
+
const keypressHandler = (e: KeyboardEvent) => this.keybindings.run(e);
|
|
614
|
+
contentDocument.addEventListener('keypress', keypressHandler, true);
|
|
615
|
+
this.toDisposeOnDetach.push(Disposable.create(() => contentDocument.removeEventListener('keypress', keypressHandler)));
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
// There is not much we could do with the security exceptions due to CORS.
|
|
619
|
+
}
|
|
620
|
+
}));
|
|
621
|
+
} catch (e) {
|
|
622
|
+
clearTimeout(this.frameLoadTimeout);
|
|
623
|
+
this.hideLoadIndicator();
|
|
624
|
+
this.showErrorBar(String(e));
|
|
625
|
+
console.log(e);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
}
|