diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts
index b3cbcb96782..65ea8acda8c 100644
--- a/src/vs/workbench/parts/html/browser/html.contribution.ts
+++ b/src/vs/workbench/parts/html/browser/html.contribution.ts
@@ -14,8 +14,6 @@ import {HtmlInput} from '../common/htmlInput';
import {HtmlPreviewPart} from 'vs/workbench/parts/html/browser/htmlPreviewPart';
import {Registry} from 'vs/platform/platform';
import {EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions} from 'vs/workbench/browser/parts/editor/baseEditor';
-
-
import {SyncDescriptor} from 'vs/platform/instantiation/common/descriptors';
// --- Register Editor
diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
index c509a8b91de..d8194ec97e7 100644
--- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
+++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
@@ -5,298 +5,122 @@
'use strict';
+import 'vs/text!./webview.html';
import {localize} from 'vs/nls';
import URI from 'vs/base/common/uri';
import {TPromise} from 'vs/base/common/winjs.base';
import {IModel, EventType} from 'vs/editor/common/editorCommon';
import {Dimension, Builder} from 'vs/base/browser/builder';
-import {empty as EmptyDisposable} from 'vs/base/common/lifecycle';
+import {empty as EmptyDisposable, IDisposable, dispose} from 'vs/base/common/lifecycle';
import {addDisposableListener} from 'vs/base/browser/dom';
import {EditorOptions, EditorInput} from 'vs/workbench/common/editor';
import {BaseEditor} from 'vs/workbench/browser/parts/editor/baseEditor';
import {Position} from 'vs/platform/editor/common/editor';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
+import {isLightTheme} from 'vs/platform/theme/common/themes';
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {BaseTextEditorModel} from 'vs/workbench/common/editor/textEditorModel';
import {HtmlInput} from 'vs/workbench/parts/html/common/htmlInput';
-import {isLightTheme} from 'vs/platform/theme/common/themes';
import {IThemeService} from 'vs/workbench/services/themes/common/themeService';
-/**
- * An implementation of editor for showing HTML content in an IFrame by leveraging the IFrameEditorInput.
- */
-export class HtmlPreviewPart extends BaseEditor {
+declare interface Webview extends HTMLElement {
+ src: string;
+ autoSize: 'on';
+ nodeintegration: 'on';
+ disablewebsecurity: 'on';
- static ID: string = 'workbench.editor.htmlPreviewPart';
+ getURL(): string;
+ getTitle(): string;
+ executeJavaScript(code: string, userGesture?: boolean, callback?: (result: any) => any);
+ send(channel: string, ...args: any[]);
+ openDevTools(): any;
+ closeDevTools(): any;
+}
- private _editorService: IWorkbenchEditorService;
- private _themeService: IThemeService;
- private _iFrameElement: HTMLIFrameElement;
- private _iFrameMessageSubscription = EmptyDisposable;
- private _iFrameBase: URI;
+class ManagedWebview {
- private _model: IModel;
- private _lastModelVersion: number;
- private _modelChangeSubscription = EmptyDisposable;
- private _themeChangeSubscription = EmptyDisposable;
+ private _webview: Webview;
+ private _ready: TPromise;
+ private _disposables: IDisposable[];
- constructor(
- @ITelemetryService telemetryService: ITelemetryService,
- @IWorkbenchEditorService editorService: IWorkbenchEditorService,
- @IWorkspaceContextService contextService: IWorkspaceContextService,
- @IThemeService themeService: IThemeService
- ) {
- super(HtmlPreviewPart.ID, telemetryService);
+ constructor(private _parent: HTMLElement, private _layoutParent: HTMLElement, private _styleElement) {
+ this._webview = document.createElement('webview');
+ this._webview.style.zIndex = '1';
+ this._webview.style.position = 'absolute';
+ this._webview.style.left = '-1e10px'; // visible but far away
+ this._webview.autoSize = 'on';
+ this._webview.nodeintegration = 'on';
+ this._webview.src = require.toUrl('./webview.html');
- this._editorService = editorService;
- this._themeService = themeService;
- this._iFrameBase = contextService.toResource('/');
+ this._ready = new TPromise(resolve => {
+ const subscription = addDisposableListener(this._webview, 'ipc-message', (event) => {
+ if (event.channel === 'webview-ready') {
+ // this._webview.openDevTools();
+ // console.info('[PID Webview] ' + event.args[0]);
+ subscription.dispose();
+ resolve(this);
+ }
+ });
+ });
+
+ this._disposables = [
+ addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) {
+ console.log(`[Embedded Page] ${e.message}`);
+ }),
+ addDisposableListener(this._webview, 'crashed', function () {
+ console.error('embedded page crashed');
+ })
+ ];
+
+ this._parent.appendChild(this._webview);
}
dispose(): void {
- // remove from dom
- const element = this._iFrameElement.parentElement;
- element.parentElement.removeChild(element);
-
- // unhook from model
- this._modelChangeSubscription.dispose();
- this._model = undefined;
-
- this._themeChangeSubscription.dispose();
+ this._disposables = dispose(this._disposables);
+ this._webview.parentElement.removeChild(this._webview);
}
- public createEditor(parent: Builder): void {
-
- // IFrame
- // this.iframeBuilder.removeProperty(IFrameEditor.RESOURCE_PROPERTY);
- this._iFrameElement = document.createElement('iframe');
- this._iFrameElement.setAttribute('frameborder', '0');
- this._iFrameElement.className = 'iframe';
-
- // Container for IFrame
- const iFrameContainerElement = document.createElement('div');
- iFrameContainerElement.className = 'iframe-container monaco-editor-background'; // Inherit the background color from selected theme
- iFrameContainerElement.tabIndex = 0; // enable focus support from the editor part (do not remove)
- iFrameContainerElement.appendChild(this._iFrameElement);
-
- parent.getHTMLElement().appendChild(iFrameContainerElement);
-
- this._themeChangeSubscription = this._themeService.onDidThemeChange(themeId => {
- if (this.isVisible()) {
- this._updateIFrameContent(true);
- }
- });
-
- this._iFrameMessageSubscription = addDisposableListener(window, 'message', e => {
-
- if (e.source !== this._iFrameElement.contentWindow) {
- return;
- }
-
- const fakeEvent = document.createEvent('KeyboardEvent'); // create a keyboard event
- Object.defineProperty(fakeEvent, 'keyCode', { // we need to set some properties that Chrome wants
- get: function() {
- return e.data.keyCode;
- }
- });
- Object.defineProperty(fakeEvent, 'which', {
- get: function() {
- return e.data.keyCode;
- }
- });
- Object.defineProperty(fakeEvent, 'target', {
- get: function() {
- return window && window.parent.document.body;
- }
- });
- fakeEvent.initKeyboardEvent('keydown', true, true, document.defaultView, null, null,
- e.data.ctrlKey, e.data.altKey, e.data.shiftKey, e.data.metaKey); // the API shape of this method is not clear to me, but it works
-
- document.dispatchEvent(fakeEvent);
- });
+ private _send(channel: string, ...args: any[]): void {
+ this._ready
+ .then(() => this._webview.send(channel, ...args))
+ .done(void 0, console.error);
}
- public layout(dimension: Dimension): void {
- let {width, height} = dimension;
- this._iFrameElement.parentElement.style.width = `${width}px`;
- this._iFrameElement.parentElement.style.height = `${height}px`;
- this._iFrameElement.style.width = `${width}px`;
- this._iFrameElement.style.height = `${height}px`;
+ set contents(value: string[]) {
+ this._send('content', value);
}
- public focus(): void {
- // this.iframeContainer.domFocus();
- this._iFrameElement.focus();
+ set baseUrl(value: string) {
+ this._send('baseUrl', value);
}
- // --- input
+ focus(): void {
+ this._send('focus');
+ }
- public getTitle(): string {
- if (!this.input) {
- return localize('iframeEditor', 'Preview Html');
+ layout(): void {
+ const {top, left, width, height} = this._layoutParent.getBoundingClientRect();
+ this._webview.style.top = `${top}px`;
+ this._webview.style.left = `${left}px`;
+ this._webview.style.width = `${width}px`;
+ this._webview.style.height = `${height}px`;
+
+ this._send('layout', width, height);
+ }
+
+ style(themeId: string): void {
+ const {color, backgroundColor, fontFamily, fontSize} = window.getComputedStyle(this._styleElement);
+
+ let value = `
+ body {
+ margin: 0;
}
- return this.input.getName();
- }
-
- public setVisible(visible: boolean, position?: Position): TPromise {
- return super.setVisible(visible, position).then(() => {
- if (visible && this._model) {
- this._modelChangeSubscription = this._model.addListener2(EventType.ModelContentChanged2, () => this._updateIFrameContent());
- this._updateIFrameContent();
- } else {
- this._modelChangeSubscription.dispose();
- }
- });
- }
-
- public changePosition(position: Position): void {
- super.changePosition(position);
-
- // reparenting an IFRAME into another DOM element yields weird results when the contents are made
- // of a string and not a URL. to be on the safe side we reload the iframe when the position changes
- // and we do it using a timeout of 0 to reload only after the position has been changed in the DOM
- setTimeout(() => {
- this._updateIFrameContent(true);
- }, 0);
- }
-
- public setInput(input: EditorInput, options: EditorOptions): TPromise {
-
- this._model = undefined;
- this._modelChangeSubscription.dispose();
- this._lastModelVersion = -1;
-
- if (!(input instanceof HtmlInput)) {
- return TPromise.wrapError('Invalid input');
- }
-
- return this._editorService.resolveEditorModel({ resource: (input).getResource() }).then(model => {
- if (model instanceof BaseTextEditorModel) {
- this._model = model.textEditorModel;
- }
-
- if (!this._model) {
- return TPromise.wrapError(localize('html.voidInput', "Invalid editor input."));
- }
-
- this._modelChangeSubscription = this._model.addListener2(EventType.ModelContentChanged2, () => this._updateIFrameContent());
- this._updateIFrameContent();
-
- return super.setInput(input, options);
- });
- }
-
- private _updateIFrameContent(refresh: boolean = false): void {
-
- if (!this._model || (!refresh && this._lastModelVersion === this._model.getVersionId())) {
- // nothing to do
- return;
- }
-
- const html = this._model.getValue();
- const iFrameDocument = this._iFrameElement.contentDocument;
-
- if (!iFrameDocument) {
- // not visible anymore
- return;
- }
-
- const parser = new DOMParser();
- const newDocument = parser.parseFromString(html, 'text/html');
- // ensure styles
- const styleElement = Integration.defaultStyle(this._iFrameElement.parentElement, this._themeService.getTheme());
- if (newDocument.head.hasChildNodes()) {
- newDocument.head.insertBefore(styleElement, newDocument.head.firstChild);
- } else {
- newDocument.head.appendChild(styleElement);
- }
- // set baseurl if possible
- if (this._iFrameBase) {
- const baseElement = document.createElement('base');
- baseElement.href = this._iFrameBase.toString();
- newDocument.head.appendChild(baseElement);
- }
- // propagate key events
- newDocument.body.appendChild(Integration.bubbleKeybindings);
-
- // write new content to iframe
- iFrameDocument.open('text/html', 'replace');
- iFrameDocument.write(newDocument.documentElement.innerHTML);
- iFrameDocument.close();
-
- this._lastModelVersion = this._model.getVersionId();
- }
-}
-
-namespace Integration {
-
- 'use strict';
-
- // scripts
-
- export const bubbleKeybindings = document.createElement('script');
- bubbleKeybindings.innerHTML = `
- var ignoredKeys = [9 /* tab */, 32 /* space */, 33 /* page up */, 34 /* page down */, 38 /* up */, 40 /* down */];
- var ignoredCtrlCmdKeys = [65 /* a */, 67 /* c */];
- var ignoredShiftKeys = [9 /* tab */];
- window.document.body.addEventListener("keydown", function(event) {
- try {
- if (!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey && ignoredKeys.some(function(i) {return i === event.keyCode;})) {
- return;
- }
- if ((event.ctrlKey || event.metaKey) && ignoredCtrlCmdKeys.some(function(i) { return i === event.keyCode; })) {
- return;
- }
- if (event.shiftKey && ignoredShiftKeys.some(function(i) { return i === event.keyCode; })) {
- return;
- }
- event.preventDefault();
- window.parent.postMessage({ which: event.which, keyCode: event.keyCode, charCode: event.charCode, metaKey: event.metaKey, altKey: event.altKey, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey }, "*");
- } catch (error) { }
- });
- function defaultPreventHandler(e) { e.preventDefault(); };
- window.document.addEventListener("dragover", defaultPreventHandler);
- window.document.addEventListener("drop", defaultPreventHandler);
- window.document.body.addEventListener("dragover", defaultPreventHandler);
- window.document.body.addEventListener("drop", defaultPreventHandler);
- `;
-
- // styles
-
- const defaultLightScrollbarStyle = [
- '::-webkit-scrollbar-thumb {',
- ' background-color: rgba(100, 100, 100, 0.4);',
- '}',
- '::-webkit-scrollbar-thumb:hover {',
- ' background-color: rgba(100, 100, 100, 0.7);',
- '}',
- '::-webkit-scrollbar-thumb:active {',
- ' background-color: rgba(0, 0, 0, 0.6);',
- '}'
- ].join('\n');
-
- const defaultDarkScrollbarStyle = [
- '::-webkit-scrollbar-thumb {',
- ' background-color: rgba(121, 121, 121, 0.4);',
- '}',
- '::-webkit-scrollbar-thumb:hover {',
- ' background-color: rgba(100, 100, 100, 0.7);',
- '}',
- '::-webkit-scrollbar-thumb:active {',
- ' background-color: rgba(85, 85, 85, 0.8);',
- '}'
- ].join('\n');
-
- export function defaultStyle(element: HTMLElement, themeId: string): HTMLStyleElement {
- const styles = window.getComputedStyle(element);
- const styleElement = document.createElement('style');
-
- styleElement.innerHTML = `* {
- color: ${styles.color};
- background: ${styles.background};
- font-family: ${styles.fontFamily};
- font-size: ${styles.fontSize};
+ * {
+ color: ${color};
+ background-color: ${backgroundColor};
+ font-family: ${fontFamily};
+ font-size: ${fontSize};
}
img {
max-width: 100%;
@@ -311,12 +135,145 @@ namespace Integration {
}
::-webkit-scrollbar {
width: 14px;
- height: 14px;
+ height: 10px;
}
- ${isLightTheme(themeId)
- ? defaultLightScrollbarStyle
- : defaultDarkScrollbarStyle}`;
+ ::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(100, 100, 100, 0.7);
+ }`;
- return styleElement;
+ if (isLightTheme(themeId)) {
+ value += `
+ ::-webkit-scrollbar-thumb {
+ background-color: rgba(100, 100, 100, 0.4);
+ }
+ ::-webkit-scrollbar-thumb:active {
+ background-color: rgba(0, 0, 0, 0.6);
+ }`;
+ } else {
+ value += `
+ ::-webkit-scrollbar-thumb {
+ background-color: rgba(121, 121, 121, 0.4);
+ }
+ ::-webkit-scrollbar-thumb:active {
+ background-color: rgba(85, 85, 85, 0.8);
+ }`;
+ }
+
+ this._send('styles', value);
+ }
+}
+
+/**
+ * An implementation of editor for showing HTML content in an IFrame by leveraging the IFrameEditorInput.
+ */
+export class HtmlPreviewPart extends BaseEditor {
+
+ static ID: string = 'workbench.editor.htmlPreviewPart';
+
+ private _editorService: IWorkbenchEditorService;
+ private _themeService: IThemeService;
+ private _webview: ManagedWebview;
+ private _container: HTMLDivElement;
+
+ private _baseUrl: URI;
+
+ private _model: IModel;
+ private _modelChangeSubscription = EmptyDisposable;
+ private _themeChangeSubscription = EmptyDisposable;
+
+ constructor(
+ @ITelemetryService telemetryService: ITelemetryService,
+ @IWorkbenchEditorService editorService: IWorkbenchEditorService,
+ @IThemeService themeService: IThemeService,
+ @IWorkspaceContextService contextService: IWorkspaceContextService
+ ) {
+ super(HtmlPreviewPart.ID, telemetryService);
+
+ this._editorService = editorService;
+ this._themeService = themeService;
+ this._baseUrl = contextService.toResource('/');
+ }
+
+ dispose(): void {
+ // remove from dom
+ this._webview.dispose();
+
+ // unhook listeners
+ this._themeChangeSubscription.dispose();
+ this._modelChangeSubscription.dispose();
+ this._model = undefined;
+ super.dispose();
+ }
+
+ public createEditor(parent: Builder): void {
+ this._container = document.createElement('div');
+ parent.getHTMLElement().appendChild(this._container);
+ }
+
+ private get webview(): ManagedWebview {
+ if (!this._webview) {
+ this._webview = new ManagedWebview(document.getElementById('workbench.main.container'),
+ this._container,
+ document.querySelector('.monaco-editor-background'));
+
+ this._webview.baseUrl = this._baseUrl && this._baseUrl.toString();
+ }
+ return this._webview;
+ }
+
+ public setVisible(visible: boolean, position?: Position): TPromise {
+ if (!visible) {
+ this._themeChangeSubscription.dispose();
+ this._modelChangeSubscription.dispose();
+ this._webview.dispose();
+ this._webview = undefined;
+ } else {
+ this._themeChangeSubscription = this._themeService.onDidThemeChange(themeId => this.webview.style(themeId));
+ this.webview.style(this._themeService.getTheme());
+ this.webview.layout();
+
+ if (this._model) {
+ this._modelChangeSubscription = this._model.addListener2(EventType.ModelContentChanged2, () => this.webview.contents = this._model.getLinesContent());
+ this.webview.contents = this._model.getLinesContent();
+ }
+ }
+ return super.setVisible(visible, position);
+ }
+
+ public layout(dimension: Dimension): void {
+ const {width, height} = dimension;
+ this._container.style.width = `${width}px`;
+ this._container.style.height = `${height}px`;
+ this.webview.layout();
+ }
+
+ public focus(): void {
+ this.webview.focus();
+ }
+
+ public setInput(input: EditorInput, options: EditorOptions): TPromise {
+
+ if (this.input === input) {
+ return TPromise.as(undefined);
+ }
+
+ this._model = undefined;
+ this._modelChangeSubscription.dispose();
+
+ if (!(input instanceof HtmlInput)) {
+ return TPromise.wrapError('Invalid input');
+ }
+
+ return this._editorService.resolveEditorModel({ resource: (input).getResource() }).then(model => {
+ if (model instanceof BaseTextEditorModel) {
+ this._model = model.textEditorModel;
+ }
+ if (!this._model) {
+ return TPromise.wrapError(localize('html.voidInput', "Invalid editor input."));
+ }
+ this._modelChangeSubscription = this._model.addListener2(EventType.ModelContentChanged2, () => this.webview.contents = this._model.getLinesContent());
+ this.webview.contents = this._model.getLinesContent();
+ return super.setInput(input, options);
+ });
}
}
diff --git a/src/vs/workbench/parts/html/browser/webview.html b/src/vs/workbench/parts/html/browser/webview.html
new file mode 100644
index 00000000000..90b4580c811
--- /dev/null
+++ b/src/vs/workbench/parts/html/browser/webview.html
@@ -0,0 +1,77 @@
+
+
+
+ Virtual Document
+
+
+
+
+
+
\ No newline at end of file