Skip to content

Commit

Permalink
[webview] open links via OpenerService
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Nov 11, 2019
1 parent 5908bc4 commit 21e10bc
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 209 deletions.
149 changes: 0 additions & 149 deletions packages/plugin-ext/src/main/browser/webview/theme-rules-service.ts

This file was deleted.

66 changes: 58 additions & 8 deletions packages/plugin-ext/src/main/browser/webview/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,21 @@ import { Deferred } from '@theia/core/lib/common/promise-util';
import { WebviewEnvironment } from './webview-environment';
import URI from '@theia/core/lib/common/uri';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { Emitter } from '@theia/core/lib/common/event';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { Schemes } from '../../../common/uri-components';

// tslint:disable:no-any

export const enum WebviewMessageChannels {
onmessage = 'onmessage',
didClickLink = 'did-click-link',
doUpdateState = 'do-update-state',
doReload = 'do-reload',
loadResource = 'load-resource',
webviewReady = 'webview-ready'
webviewReady = 'webview-ready',
didKeydown = 'did-keydown'
}

export interface WebviewContentOptions {
Expand All @@ -45,12 +52,6 @@ export interface WebviewContentOptions {
readonly enableCommandUris?: boolean;
}

export interface WebviewEvents {
onMessage?(message: any): void;
onKeyboardEvent?(e: KeyboardEvent): void;
onLoad?(contentDocument: Document): void;
}

@injectable()
export class WebviewWidgetIdentifier {
id: string;
Expand All @@ -61,6 +62,13 @@ export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoi
@injectable()
export class WebviewWidget extends BaseWidget implements StatefulWidget {

private static readonly standardSupportedLinkSchemes = new Set([
Schemes.HTTP,
Schemes.HTTPS,
Schemes.MAILTO,
Schemes.VSCODE
]);

static FACTORY_ID = 'plugin-webview';

protected element: HTMLIFrameElement;
Expand All @@ -85,6 +93,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;

@inject(OpenerService)
protected readonly openerService: OpenerService;

@inject(KeybindingRegistry)
protected readonly keybindings: KeybindingRegistry;

viewState: WebviewPanelViewState = {
visible: false,
active: false,
Expand All @@ -97,17 +111,21 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {

viewType: string;
options: WebviewPanelOptions = {};
eventDelegate: WebviewEvents = {};

protected readonly ready = new Deferred<void>();

protected readonly onMessageEmitter = new Emitter<any>();
readonly onMessage = this.onMessageEmitter.event;

@postConstruct()
protected init(): void {
this.node.tabIndex = 0;
this.id = WebviewWidget.FACTORY_ID + ':' + this.identifier.id;
this.title.closable = true;
this.addClass(WebviewWidget.Styles.WEBVIEW);

this.toDispose.push(this.onMessageEmitter);

this.transparentOverlay = document.createElement('div');
this.transparentOverlay.classList.add(MiniBrowserContentStyle.TRANSPARENT_OVERLAY);
this.transparentOverlay.style.display = 'none';
Expand Down Expand Up @@ -139,6 +157,8 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
this.ready.resolve();
});
this.toDispose.push(subscription);
this.toDispose.push(this.on(WebviewMessageChannels.onmessage, (data: any) => this.onMessageEmitter.fire(data)));
this.toDispose.push(this.on(WebviewMessageChannels.didClickLink, (uri: string) => this.openLink(new URI(uri))));
this.toDispose.push(this.on(WebviewMessageChannels.doUpdateState, (state: any) => {
this.state = state;
}));
Expand All @@ -149,6 +169,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
const uri = new URI(normalizedPath.replace(/^\/(\w+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path));
this.loadResource(rawPath, uri);
}));
this.toDispose.push(this.on(WebviewMessageChannels.didKeydown, (data: KeyboardEvent) => {
// Electron: workaround for https://github.com/electron/electron/issues/14258
// We have to detect keyboard events in the <webview> and dispatch them to our
// keybinding service because these events do not bubble to the parent window anymore.
this.dispatchKeyDown(data);
}));
}

setContentOptions(contentOptions: WebviewContentOptions): void {
Expand Down Expand Up @@ -193,6 +219,30 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
this.doUpdateContent();
}

protected dispatchKeyDown(event: KeyboardEventInit): void {
// Create a fake KeyboardEvent from the data provided
const emulatedKeyboardEvent = new KeyboardEvent('keydown', event);
// Force override the target
Object.defineProperty(emulatedKeyboardEvent, 'target', {
get: () => this.element,
});
// And re-dispatch
this.keybindings.run(emulatedKeyboardEvent);
}

protected openLink(link: URI): void {
if (this.isSupportedLink(link)) {
open(this.openerService, link);
}
}

protected isSupportedLink(link: URI): boolean {
if (WebviewWidget.standardSupportedLinkSchemes.has(link.scheme)) {
return true;
}
return !!this.contentOptions.enableCommandUris && link.scheme === Schemes.COMMAND;
}

protected async loadResource(requestPath: string, uri: URI): Promise<void> {
try {
const normalizedUri = this.normalizeRequestUri(uri);
Expand Down
56 changes: 4 additions & 52 deletions packages/plugin-ext/src/main/browser/webviews-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,25 @@ import { interfaces } from 'inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { WebviewWidget, WebviewWidgetIdentifier } from './webview/webview';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ThemeRulesService } from './webview/theme-rules-service';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ViewColumnService } from './view-column-service';
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { JSONExt } from '@phosphor/coreutils/lib/json';
import { Mutable } from '@theia/core/lib/common/types';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';

export class WebviewsMainImpl implements WebviewsMain, Disposable {

private readonly proxy: WebviewsExt;
protected readonly shell: ApplicationShell;
protected readonly widgets: WidgetManager;
protected readonly pluginService: HostedPluginSupport;
protected readonly viewColumnService: ViewColumnService;
protected readonly keybindingRegistry: KeybindingRegistry;
protected readonly themeService = ThemeService.get();
protected readonly themeRulesService = ThemeRulesService.get();
private readonly toDispose = new DisposableCollection();

constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT);
this.shell = container.get(ApplicationShell);
this.keybindingRegistry = container.get(KeybindingRegistry);
this.viewColumnService = container.get(ViewColumnService);
this.widgets = container.get(WidgetManager);
this.pluginService = container.get(HostedPluginSupport);
Expand Down Expand Up @@ -82,50 +74,11 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {

protected hookWebview(view: WebviewWidget): void {
const handle = view.identifier.id;
const toDisposeOnClose = new DisposableCollection();
const toDisposeOnLoad = new DisposableCollection();
this.toDispose.push(view.onMessage(data => this.proxy.$onMessage(handle, data)));

view.eventDelegate = {
// TODO review callbacks
onMessage: m => {
this.proxy.$onMessage(handle, m);
},
onKeyboardEvent: e => {
this.keybindingRegistry.run(e);
},
onLoad: contentDocument => {
const styleId = 'webview-widget-theme';
let styleElement: HTMLStyleElement | null | undefined;
if (!toDisposeOnLoad.disposed) {
// if reload the frame
toDisposeOnLoad.dispose();
styleElement = <HTMLStyleElement>contentDocument.getElementById(styleId);
}
toDisposeOnClose.push(toDisposeOnLoad);
if (!styleElement) {
const parent = contentDocument.head ? contentDocument.head : contentDocument.body;
styleElement = this.themeRulesService.createStyleSheet(parent);
styleElement.id = styleId;
parent.appendChild(styleElement);
}

this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules());
contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
toDisposeOnLoad.push(this.themeService.onThemeChange(() => {
this.themeRulesService.setRules(<HTMLElement>styleElement, this.themeRulesService.getCurrentThemeRules());
contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
}));
}
};
this.toDispose.push(Disposable.create(() => view.eventDelegate = {}));

view.disposed.connect(() => {
toDisposeOnClose.dispose();
if (!this.toDispose.disposed) {
this.proxy.$onDidDisposeWebviewPanel(handle);
}
});
toDisposeOnClose.push(Disposable.create(() => this.themeRulesService.setIconPath(handle, undefined)));
const onDispose = () => this.proxy.$onDidDisposeWebviewPanel(handle);
view.disposed.connect(onDispose);
this.toDispose.push(Disposable.create(() => view.disposed.disconnect(onDispose)));
}

private async addOrReattachWidget(handle: string, showOptions: WebviewPanelShowOptions): Promise<void> {
Expand Down Expand Up @@ -205,7 +158,6 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
async $setIconPath(handle: string, iconPath: { light: string; dark: string; } | string | undefined): Promise<void> {
const webview = await this.getWebview(handle);
webview.setIconClass(iconPath ? `webview-icon ${handle}-file-icon` : '');
this.themeRulesService.setIconPath(handle, iconPath);
}

async $setHtml(handle: string, value: string): Promise<void> {
Expand Down

0 comments on commit 21e10bc

Please sign in to comment.