From 0f21cd296ef5445891cb4776e16d7a718899e219 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 4 Apr 2016 15:52:45 +0200 Subject: [PATCH 1/7] wip --- .../parts/html/browser/html.contribution.ts | 17 ++- .../parts/html/browser/htmlWebviewPart.ts | 121 ++++++++++++++++++ .../workbench/parts/html/browser/webview.html | 24 ++++ 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 src/vs/workbench/parts/html/browser/htmlWebviewPart.ts create mode 100644 src/vs/workbench/parts/html/browser/webview.html diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index b3cbcb96782..8297dfcba83 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -11,7 +11,8 @@ import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/edito import URI from 'vs/base/common/uri'; import {Position as EditorPosition} from 'vs/platform/editor/common/editor'; import {HtmlInput} from '../common/htmlInput'; -import {HtmlPreviewPart} from 'vs/workbench/parts/html/browser/htmlPreviewPart'; +// import {HtmlPreviewPart} from 'vs/workbench/parts/html/browser/htmlPreviewPart'; +import {WebviewPart} from 'vs/workbench/parts/html/browser/htmlWebviewPart'; import {Registry} from 'vs/platform/platform'; import {EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions} from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -19,10 +20,16 @@ import {EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions} from import {SyncDescriptor} from 'vs/platform/instantiation/common/descriptors'; // --- Register Editor -(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(HtmlPreviewPart.ID, - localize('html.editor.label', "Html Preview"), - 'vs/workbench/parts/html/browser/htmlPreviewPart', - 'HtmlPreviewPart'), +// (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(HtmlPreviewPart.ID, +// localize('html.editor.label', "Html Preview"), +// 'vs/workbench/parts/html/browser/htmlPreviewPart', +// 'HtmlPreviewPart'), +// [new SyncDescriptor(HtmlInput)]); + +(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(WebviewPart.ID, + localize('webview.editor.label', "[WEBVIEW] Html Preview"), + 'vs/workbench/parts/html/browser/htmlWebviewPart', + 'WebviewPart'), [new SyncDescriptor(HtmlInput)]); // --- Register Commands diff --git a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts new file mode 100644 index 00000000000..95cbbed4c3a --- /dev/null +++ b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'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 {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 {IStorageService/*, StorageEventType, StorageScope*/} from 'vs/platform/storage/common/storage'; +import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; +// import {BaseTextEditorModel} from 'vs/workbench/common/editor/textEditorModel'; +// import {Preferences} from 'vs/workbench/common/constants'; +import {HtmlInput} from 'vs/workbench/parts/html/common/htmlInput'; +// import {isLightTheme} from 'vs/platform/theme/common/themes'; +// import {DEFAULT_THEME_ID} from 'vs/workbench/services/themes/common/themeService'; + +declare interface Webview extends HTMLElement { + src: string; + autoSize: 'on'; + nodeintegration: 'on'; + disablewebsecurity: 'on'; + + getURL(): string; + getTitle(): string; + executeJavaScript(code: string, userGesture?: boolean, callback?: (result: any) => any); + send(channel: string, ...args: any[]); + openDevTools(): any; + closeDevTools(): any; +} + +/** + * An implementation of editor for showing HTML content in an IFrame by leveraging the IFrameEditorInput. + */ +export class WebviewPart extends BaseEditor { + + static ID: string = 'workbench.editor.webviewPart'; + + private _webview: TPromise; + private _editorService: IWorkbenchEditorService; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IWorkbenchEditorService editorService: IWorkbenchEditorService, + @IStorageService storageService: IStorageService, + @IWorkspaceContextService contextService: IWorkspaceContextService + ) { + super(WebviewPart.ID, telemetryService); + + this._editorService = editorService; + // this._storageService = storageService; + // this._iFrameBase = contextService.toResource('/'); + } + + public createEditor(parent: Builder): void { + const webview = document.createElement('webview'); + webview.autoSize = 'on'; + webview.nodeintegration = 'on'; + // webview.disablewebsecurity = 'on'; + webview.setAttribute('allowDisplayingInsecureContent', 'false'); + webview.setAttribute('allowRunningInsecureContent', 'false'); + webview.src = require.toUrl('./webview.html'); + + parent.getHTMLElement().appendChild(webview); + + this._webview = new TPromise((resolve, reject) => { + const onDomReady = () => { + webview.openDevTools(); + webview.removeEventListener('dom-ready', onDomReady); + addDisposableListener(webview, 'ipc-message', (event) => { + console.log('IN', event); + }); + resolve(webview); + }; + webview.addEventListener('dom-ready', onDomReady); + }); + } + + public layout(dimension: Dimension): void { + const {width, height} = dimension; + this._webview.then(value => { + value.style.width = `${width}px`; + value.style.height = `${height}px`; + value.send('layout', width, height); + }); + } + + // public focus(): void { + // this._webview.focus(); + // } + + public setInput(input: EditorInput, options: EditorOptions): TPromise { + + if (!(input instanceof HtmlInput)) { + return TPromise.wrapError('Invalid input'); + } + + return this._editorService.resolveEditorModel({ resource: (input).getResource() }).then(model => { + return this._webview.then(value => { + + value.send('content', (model.textEditorModel).getValue(), function () { + console.log(arguments); + }); + + 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..f7c9bb054cc --- /dev/null +++ b/src/vs/workbench/parts/html/browser/webview.html @@ -0,0 +1,24 @@ + + +Virtual Document + + + + \ No newline at end of file From 50f5b09e6fcf7f5a3b3f7f294f085c9c03a19ef3 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 5 Apr 2016 16:40:46 +0200 Subject: [PATCH 2/7] webview work --- .../parts/html/browser/htmlWebviewPart.ts | 203 ++++++++++++++---- .../workbench/parts/html/browser/webview.html | 71 +++++- 2 files changed, 222 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts index 95cbbed4c3a..607caef87d4 100644 --- a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts @@ -6,25 +6,25 @@ 'use strict'; import 'vs/text!./webview.html'; -// import {localize} from 'vs/nls'; -// import URI from 'vs/base/common/uri'; +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 {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} 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 {Position} from 'vs/platform/editor/common/editor'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; -import {IStorageService/*, StorageEventType, StorageScope*/} from 'vs/platform/storage/common/storage'; +import {IStorageService, StorageEventType, StorageScope} from 'vs/platform/storage/common/storage'; +import {DEFAULT_THEME_ID} from 'vs/workbench/services/themes/common/themeService'; +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 {Preferences} from 'vs/workbench/common/constants'; +import {BaseTextEditorModel} from 'vs/workbench/common/editor/textEditorModel'; +import {Preferences} from 'vs/workbench/common/constants'; import {HtmlInput} from 'vs/workbench/parts/html/common/htmlInput'; -// import {isLightTheme} from 'vs/platform/theme/common/themes'; -// import {DEFAULT_THEME_ID} from 'vs/workbench/services/themes/common/themeService'; declare interface Webview extends HTMLElement { src: string; @@ -47,8 +47,15 @@ export class WebviewPart extends BaseEditor { static ID: string = 'workbench.editor.webviewPart'; - private _webview: TPromise; private _editorService: IWorkbenchEditorService; + private _storageService: IStorageService; + private _container: HTMLDivElement; + private _webview: TPromise; + private _baseUrl: URI; + + private _model: IModel; + private _modelChangeSubscription = EmptyDisposable; + private _themeChangeSubscription = EmptyDisposable; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -59,63 +66,175 @@ export class WebviewPart extends BaseEditor { super(WebviewPart.ID, telemetryService); this._editorService = editorService; - // this._storageService = storageService; - // this._iFrameBase = contextService.toResource('/'); + this._storageService = storageService; + this._baseUrl = contextService.toResource('/'); + } + + dispose(): void { + // remove from dom + this._webview.then(webview => webview.parentElement.removeChild(webview)); + + // unhook listeners + this._themeChangeSubscription.dispose(); + this._modelChangeSubscription.dispose(); + this._model = undefined; + super.dispose(); } public createEditor(parent: Builder): void { - const webview = document.createElement('webview'); - webview.autoSize = 'on'; - webview.nodeintegration = 'on'; - // webview.disablewebsecurity = 'on'; - webview.setAttribute('allowDisplayingInsecureContent', 'false'); - webview.setAttribute('allowRunningInsecureContent', 'false'); - webview.src = require.toUrl('./webview.html'); - parent.getHTMLElement().appendChild(webview); + this._container = document.createElement('div'); + parent.getHTMLElement().appendChild(this._container); this._webview = new TPromise((resolve, reject) => { - const onDomReady = () => { + + const webview = document.createElement('webview'); + webview.style.position = 'absolute'; + webview.style.zIndex = '1'; + webview.style.visibility = 'hidden'; + webview.autoSize = 'on'; + webview.nodeintegration = 'on'; + webview.src = require.toUrl('./webview.html'); + + const sub1 = addDisposableListener(webview, 'dom-ready', () => { webview.openDevTools(); - webview.removeEventListener('dom-ready', onDomReady); - addDisposableListener(webview, 'ipc-message', (event) => { - console.log('IN', event); - }); - resolve(webview); - }; - webview.addEventListener('dom-ready', onDomReady); + }); + + const sub2 = addDisposableListener(webview, 'ipc-message', (event) => { + if (event.channel === 'webview-ready') { + sub1.dispose(); + sub2.dispose(); + + webview.send('init', { + baseUrl: this._baseUrl && this._baseUrl.toString(), + styles: this._getDefaultStyles() + }); + resolve(webview); + } + }); + + document.getElementById('workbench.main.container').appendChild(webview); + }); + + this._themeChangeSubscription = this._storageService.addListener2(StorageEventType.STORAGE, event => { + if (event.key === Preferences.THEME && this.isVisible()) { + this._sendToWebview('updateStyles', this._getDefaultStyles()); + } + }); + } + + + public setVisible(visible: boolean, position?: Position): TPromise { + return this._webview.then(value => { + value.style.visibility = visible ? 'inherit' : 'hidden'; + 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.then(value => { - value.style.width = `${width}px`; - value.style.height = `${height}px`; + const rect = this._container.getBoundingClientRect(); + value.style.top = `${rect.top}px`; + value.style.left = `${rect.left}px`; + value.style.width = `${rect.width}px`; + value.style.height = `${rect.height}px`; value.send('layout', width, height); }); } - // public focus(): void { - // this._webview.focus(); - // } + public focus(): void { + this._sendToWebview('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 => { - return this._webview.then(value => { - - value.send('content', (model.textEditorModel).getValue(), function () { - console.log(arguments); - }); - - return super.setInput(input, options); - }); + 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._updateFromModel()); + this._updateFromModel(); + return super.setInput(input, options); }); } + private _updateFromModel(): void { + this._sendToWebview('content', this._model.getLinesContent()); + } + + private _sendToWebview(channel:string, ...args: any[]): void { + this._webview.then(webview => webview.send(channel, ...args)).done(undefined, console.error); + } + + private _getDefaultStyles():string { + // const {color, background, fontFamily, fontSize} = window.getComputedStyle(this._container); + const {color, backgroundColor, fontFamily, fontSize} = window.getComputedStyle(document.querySelector('.monaco-editor-background')); + + let value = ` + body { + margin: 0; + } + * { + color: ${color}; + background-color: ${backgroundColor}; + font-family: ${fontFamily}; + font-size: ${fontSize}; + } + img { + max-width: 100%; + max-height: 100%; + } + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + ::-webkit-scrollbar { + width: 14px; + height: 10px; + } + ::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + }`; + + if (isLightTheme(this._storageService.get(Preferences.THEME, StorageScope.GLOBAL, DEFAULT_THEME_ID))) { + 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); + }`; + } + + return value; + } } diff --git a/src/vs/workbench/parts/html/browser/webview.html b/src/vs/workbench/parts/html/browser/webview.html index f7c9bb054cc..b53149d6972 100644 --- a/src/vs/workbench/parts/html/browser/webview.html +++ b/src/vs/workbench/parts/html/browser/webview.html @@ -1,24 +1,75 @@ -Virtual Document + + Virtual Document + - + + \ No newline at end of file From 0ec9334c6bd619094d3104f7d22c9c3890b7418b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Apr 2016 09:16:11 +0200 Subject: [PATCH 3/7] adopt theme service in webview part --- .../parts/html/browser/htmlWebviewPart.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts index 607caef87d4..3f4cde6f9c5 100644 --- a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts @@ -18,13 +18,11 @@ 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 {IStorageService, StorageEventType, StorageScope} from 'vs/platform/storage/common/storage'; -import {DEFAULT_THEME_ID} from 'vs/workbench/services/themes/common/themeService'; 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 {Preferences} from 'vs/workbench/common/constants'; import {HtmlInput} from 'vs/workbench/parts/html/common/htmlInput'; +import {IThemeService} from 'vs/workbench/services/themes/common/themeService'; declare interface Webview extends HTMLElement { src: string; @@ -48,7 +46,7 @@ export class WebviewPart extends BaseEditor { static ID: string = 'workbench.editor.webviewPart'; private _editorService: IWorkbenchEditorService; - private _storageService: IStorageService; + private _themeService: IThemeService; private _container: HTMLDivElement; private _webview: TPromise; private _baseUrl: URI; @@ -60,13 +58,13 @@ export class WebviewPart extends BaseEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IStorageService storageService: IStorageService, + @IThemeService themeService: IThemeService, @IWorkspaceContextService contextService: IWorkspaceContextService ) { super(WebviewPart.ID, telemetryService); this._editorService = editorService; - this._storageService = storageService; + this._themeService = themeService; this._baseUrl = contextService.toResource('/'); } @@ -116,14 +114,11 @@ export class WebviewPart extends BaseEditor { document.getElementById('workbench.main.container').appendChild(webview); }); - this._themeChangeSubscription = this._storageService.addListener2(StorageEventType.STORAGE, event => { - if (event.key === Preferences.THEME && this.isVisible()) { - this._sendToWebview('updateStyles', this._getDefaultStyles()); - } + this._themeChangeSubscription = this._themeService.onDidThemeChange(() => { + this._sendToWebview('updateStyles', this._getDefaultStyles()); }); } - public setVisible(visible: boolean, position?: Position): TPromise { return this._webview.then(value => { value.style.visibility = visible ? 'inherit' : 'hidden'; @@ -217,7 +212,7 @@ export class WebviewPart extends BaseEditor { background-color: rgba(100, 100, 100, 0.7); }`; - if (isLightTheme(this._storageService.get(Preferences.THEME, StorageScope.GLOBAL, DEFAULT_THEME_ID))) { + if (isLightTheme(this._themeService.getTheme())) { value += ` ::-webkit-scrollbar-thumb { background-color: rgba(100, 100, 100, 0.4); From 2d4350e0fada8da22b7edb44718603314076c0d7 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Apr 2016 11:27:53 +0200 Subject: [PATCH 4/7] refactor things into ManagedWebview --- .../parts/html/browser/htmlWebviewPart.ts | 296 ++++++++++-------- .../workbench/parts/html/browser/webview.html | 16 +- 2 files changed, 179 insertions(+), 133 deletions(-) diff --git a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts index 3f4cde6f9c5..bc3a900372a 100644 --- a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts @@ -11,7 +11,7 @@ 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'; @@ -38,150 +38,79 @@ declare interface Webview extends HTMLElement { closeDevTools(): any; } -/** - * An implementation of editor for showing HTML content in an IFrame by leveraging the IFrameEditorInput. - */ -export class WebviewPart extends BaseEditor { +class ManagedWebview { - static ID: string = 'workbench.editor.webviewPart'; + private _webview: Webview; + private _ready: TPromise; + private _disposables: IDisposable[]; - private _editorService: IWorkbenchEditorService; - private _themeService: IThemeService; - private _container: HTMLDivElement; - private _webview: TPromise; - private _baseUrl: URI; + 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'); - private _model: IModel; - private _modelChangeSubscription = EmptyDisposable; - private _themeChangeSubscription = EmptyDisposable; + 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); + } + }); + }); - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IThemeService themeService: IThemeService, - @IWorkspaceContextService contextService: IWorkspaceContextService - ) { - super(WebviewPart.ID, telemetryService); + 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._editorService = editorService; - this._themeService = themeService; - this._baseUrl = contextService.toResource('/'); + this._parent.appendChild(this._webview); } dispose(): void { - // remove from dom - this._webview.then(webview => webview.parentElement.removeChild(webview)); - - // unhook listeners - this._themeChangeSubscription.dispose(); - this._modelChangeSubscription.dispose(); - this._model = undefined; - super.dispose(); + this._disposables = dispose(this._disposables); + this._webview.parentElement.removeChild(this._webview); } - public createEditor(parent: Builder): void { - - this._container = document.createElement('div'); - parent.getHTMLElement().appendChild(this._container); - - this._webview = new TPromise((resolve, reject) => { - - const webview = document.createElement('webview'); - webview.style.position = 'absolute'; - webview.style.zIndex = '1'; - webview.style.visibility = 'hidden'; - webview.autoSize = 'on'; - webview.nodeintegration = 'on'; - webview.src = require.toUrl('./webview.html'); - - const sub1 = addDisposableListener(webview, 'dom-ready', () => { - webview.openDevTools(); - }); - - const sub2 = addDisposableListener(webview, 'ipc-message', (event) => { - if (event.channel === 'webview-ready') { - sub1.dispose(); - sub2.dispose(); - - webview.send('init', { - baseUrl: this._baseUrl && this._baseUrl.toString(), - styles: this._getDefaultStyles() - }); - resolve(webview); - } - }); - - document.getElementById('workbench.main.container').appendChild(webview); - }); - - this._themeChangeSubscription = this._themeService.onDidThemeChange(() => { - this._sendToWebview('updateStyles', this._getDefaultStyles()); - }); + private _send(channel: string, ...args: any[]): void { + this._ready + .then(() => this._webview.send(channel, ...args)) + .done(void 0, console.error); } - public setVisible(visible: boolean, position?: Position): TPromise { - return this._webview.then(value => { - value.style.visibility = visible ? 'inherit' : 'hidden'; - return super.setVisible(visible, position); - }); + set contents(value: string[]) { + this._send('content', value); } - public layout(dimension: Dimension): void { - const {width, height} = dimension; - this._container.style.width = `${width}px`; - this._container.style.height = `${height}px`; - - this._webview.then(value => { - const rect = this._container.getBoundingClientRect(); - value.style.top = `${rect.top}px`; - value.style.left = `${rect.left}px`; - value.style.width = `${rect.width}px`; - value.style.height = `${rect.height}px`; - value.send('layout', width, height); - }); + set baseUrl(value: string) { + this._send('baseUrl', value); } - public focus(): void { - this._sendToWebview('focus'); + focus(): void { + this._send('focus'); } - public setInput(input: EditorInput, options: EditorOptions): TPromise { + 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`; - 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._updateFromModel()); - this._updateFromModel(); - return super.setInput(input, options); - }); + this._send('layout', width, height); } - private _updateFromModel(): void { - this._sendToWebview('content', this._model.getLinesContent()); - } - - private _sendToWebview(channel:string, ...args: any[]): void { - this._webview.then(webview => webview.send(channel, ...args)).done(undefined, console.error); - } - - private _getDefaultStyles():string { - // const {color, background, fontFamily, fontSize} = window.getComputedStyle(this._container); - const {color, backgroundColor, fontFamily, fontSize} = window.getComputedStyle(document.querySelector('.monaco-editor-background')); + style(themeId: string): void { + const {color, backgroundColor, fontFamily, fontSize} = window.getComputedStyle(this._styleElement); let value = ` body { @@ -212,7 +141,7 @@ export class WebviewPart extends BaseEditor { background-color: rgba(100, 100, 100, 0.7); }`; - if (isLightTheme(this._themeService.getTheme())) { + if (isLightTheme(themeId)) { value += ` ::-webkit-scrollbar-thumb { background-color: rgba(100, 100, 100, 0.4); @@ -230,6 +159,121 @@ export class WebviewPart extends BaseEditor { }`; } - return value; + this._send('styles', value); + } +} + +/** + * An implementation of editor for showing HTML content in an IFrame by leveraging the IFrameEditorInput. + */ +export class WebviewPart extends BaseEditor { + + static ID: string = 'workbench.editor.webviewPart'; + + 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(WebviewPart.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); + } + + 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 index b53149d6972..90b4580c811 100644 --- a/src/vs/workbench/parts/html/browser/webview.html +++ b/src/vs/workbench/parts/html/browser/webview.html @@ -3,7 +3,7 @@ Virtual Document - + From b6c59c2261283feb08e7a01c02d42126e23fd931 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Apr 2016 12:19:42 +0200 Subject: [PATCH 5/7] make webview private --- src/vs/workbench/parts/html/browser/htmlWebviewPart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts index bc3a900372a..49fd89e53e9 100644 --- a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts @@ -210,7 +210,7 @@ export class WebviewPart extends BaseEditor { parent.getHTMLElement().appendChild(this._container); } - get webview(): ManagedWebview { + private get webview(): ManagedWebview { if (!this._webview) { this._webview = new ManagedWebview(document.getElementById('workbench.main.container'), this._container, From eaeae0db74ac86f377648eef496744592138a76b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Apr 2016 12:20:41 +0200 Subject: [PATCH 6/7] don't reveal dev tools --- src/vs/workbench/parts/html/browser/htmlWebviewPart.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts index 49fd89e53e9..d235dde468d 100644 --- a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts @@ -56,8 +56,8 @@ class ManagedWebview { 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]); + // this._webview.openDevTools(); + // console.info('[PID Webview] ' + event.args[0]); subscription.dispose(); resolve(this); } From 6e7e098eced6c01c43408072912fdcdc7cc74092 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Apr 2016 12:23:02 +0200 Subject: [PATCH 7/7] remove old preview part, use new one --- .../parts/html/browser/html.contribution.ts | 19 +- .../parts/html/browser/htmlPreviewPart.ts | 483 ++++++++---------- .../parts/html/browser/htmlWebviewPart.ts | 279 ---------- 3 files changed, 225 insertions(+), 556 deletions(-) delete mode 100644 src/vs/workbench/parts/html/browser/htmlWebviewPart.ts diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index 8297dfcba83..65ea8acda8c 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -11,25 +11,16 @@ import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/edito import URI from 'vs/base/common/uri'; import {Position as EditorPosition} from 'vs/platform/editor/common/editor'; import {HtmlInput} from '../common/htmlInput'; -// import {HtmlPreviewPart} from 'vs/workbench/parts/html/browser/htmlPreviewPart'; -import {WebviewPart} from 'vs/workbench/parts/html/browser/htmlWebviewPart'; +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 -// (Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(HtmlPreviewPart.ID, -// localize('html.editor.label', "Html Preview"), -// 'vs/workbench/parts/html/browser/htmlPreviewPart', -// 'HtmlPreviewPart'), -// [new SyncDescriptor(HtmlInput)]); - -(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(WebviewPart.ID, - localize('webview.editor.label', "[WEBVIEW] Html Preview"), - 'vs/workbench/parts/html/browser/htmlWebviewPart', - 'WebviewPart'), +(Registry.as(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(HtmlPreviewPart.ID, + localize('html.editor.label', "Html Preview"), + 'vs/workbench/parts/html/browser/htmlPreviewPart', + 'HtmlPreviewPart'), [new SyncDescriptor(HtmlInput)]); // --- Register Commands 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/htmlWebviewPart.ts b/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts deleted file mode 100644 index d235dde468d..00000000000 --- a/src/vs/workbench/parts/html/browser/htmlWebviewPart.ts +++ /dev/null @@ -1,279 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'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, 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 {IThemeService} from 'vs/workbench/services/themes/common/themeService'; - -declare interface Webview extends HTMLElement { - src: string; - autoSize: 'on'; - nodeintegration: 'on'; - disablewebsecurity: 'on'; - - getURL(): string; - getTitle(): string; - executeJavaScript(code: string, userGesture?: boolean, callback?: (result: any) => any); - send(channel: string, ...args: any[]); - openDevTools(): any; - closeDevTools(): any; -} - -class ManagedWebview { - - private _webview: Webview; - private _ready: TPromise; - private _disposables: IDisposable[]; - - 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._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 { - this._disposables = dispose(this._disposables); - this._webview.parentElement.removeChild(this._webview); - } - - private _send(channel: string, ...args: any[]): void { - this._ready - .then(() => this._webview.send(channel, ...args)) - .done(void 0, console.error); - } - - set contents(value: string[]) { - this._send('content', value); - } - - set baseUrl(value: string) { - this._send('baseUrl', value); - } - - focus(): void { - this._send('focus'); - } - - 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; - } - * { - color: ${color}; - background-color: ${backgroundColor}; - font-family: ${fontFamily}; - font-size: ${fontSize}; - } - img { - max-width: 100%; - max-height: 100%; - } - a:focus, - input:focus, - select:focus, - textarea:focus { - outline: 1px solid -webkit-focus-ring-color; - outline-offset: -1px; - } - ::-webkit-scrollbar { - width: 14px; - height: 10px; - } - ::-webkit-scrollbar-thumb:hover { - background-color: rgba(100, 100, 100, 0.7); - }`; - - 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 WebviewPart extends BaseEditor { - - static ID: string = 'workbench.editor.webviewPart'; - - 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(WebviewPart.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); - }); - } -}