diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
index 870a67eab3b16..7e45b697315e4 100644
--- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
+++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
@@ -5,16 +5,18 @@
'use strict';
-// import 'vs/css!./media/iframeeditor';
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 {cAll} 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 {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';
@@ -22,6 +24,7 @@ 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';
+
/**
* An implementation of editor for showing HTML content in an IFrame by leveraging the IFrameEditorInput.
*/
@@ -32,33 +35,37 @@ export class HtmlPreviewPart extends BaseEditor {
private _editorService: IWorkbenchEditorService;
private _storageService: IStorageService;
private _iFrameElement: HTMLIFrameElement;
+ private _iFrameMessageSubscription = EmptyDisposable;
+ private _iFrameBase: URI;
private _model: IModel;
- private _modelChangeUnbind: Function;
private _lastModelVersion: number;
- private _themeChangeUnbind: Function;
+ private _modelChangeSubscription = EmptyDisposable;
+ private _themeChangeSubscription = EmptyDisposable;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
- @IStorageService storageService: IStorageService
+ @IStorageService storageService: IStorageService,
+ @IWorkspaceContextService contextService: IWorkspaceContextService
) {
super(HtmlPreviewPart.ID, telemetryService);
this._editorService = editorService;
this._storageService = storageService;
+ this._iFrameBase = contextService.toResource('/');
}
dispose(): void {
- // remove from dome
+ // remove from dom
const element = this._iFrameElement.parentElement;
element.parentElement.removeChild(element);
// unhook from model
- this._modelChangeUnbind = cAll(this._modelChangeUnbind);
+ this._modelChangeSubscription.dispose();
this._model = undefined;
- this._themeChangeUnbind = cAll(this._themeChangeUnbind);
+ this._themeChangeSubscription.dispose();
}
public createEditor(parent: Builder): void {
@@ -77,11 +84,39 @@ export class HtmlPreviewPart extends BaseEditor {
parent.getHTMLElement().appendChild(iFrameContainerElement);
- this._themeChangeUnbind = this._storageService.addListener(StorageEventType.STORAGE, event => {
+ this._themeChangeSubscription = this._storageService.addListener2(StorageEventType.STORAGE, event => {
if (event.key === Preferences.THEME && 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);
+ });
}
public layout(dimension: Dimension): void {
@@ -109,10 +144,10 @@ export class HtmlPreviewPart extends BaseEditor {
public setVisible(visible: boolean, position?: Position): TPromise {
return super.setVisible(visible, position).then(() => {
if (visible && this._model) {
- this._modelChangeUnbind = this._model.addListener(EventType.ModelContentChanged2, () => this._updateIFrameContent());
+ this._modelChangeSubscription = this._model.addListener2(EventType.ModelContentChanged2, () => this._updateIFrameContent());
this._updateIFrameContent();
} else {
- this._modelChangeUnbind = cAll(this._modelChangeUnbind);
+ this._modelChangeSubscription.dispose();
}
});
}
@@ -131,7 +166,7 @@ export class HtmlPreviewPart extends BaseEditor {
public setInput(input: EditorInput, options: EditorOptions): TPromise {
this._model = undefined;
- this._modelChangeUnbind = cAll(this._modelChangeUnbind);
+ this._modelChangeSubscription.dispose();
this._lastModelVersion = -1;
if (!(input instanceof HtmlInput)) {
@@ -147,7 +182,7 @@ export class HtmlPreviewPart extends BaseEditor {
return TPromise.wrapError(localize('html.voidInput', "Invalid editor input."));
}
- this._modelChangeUnbind = this._model.addListener(EventType.ModelContentChanged2, () => this._updateIFrameContent());
+ this._modelChangeSubscription = this._model.addListener2(EventType.ModelContentChanged2, () => this._updateIFrameContent());
this._updateIFrameContent();
return super.setInput(input, options);
@@ -169,30 +204,28 @@ export class HtmlPreviewPart extends BaseEditor {
return;
}
- // the very first time we load just our script
- // to integrate with the outside world
- if ((iFrameDocument.firstChild).innerHTML === '') {
- iFrameDocument.open('text/html', 'replace');
- iFrameDocument.write(Integration.defaultHtml());
- iFrameDocument.close();
- }
-
- // diff a little against the current input and the new state
const parser = new DOMParser();
const newDocument = parser.parseFromString(html, 'text/html');
+ // ensure styles
const styleElement = Integration.defaultStyle(this._iFrameElement.parentElement, this._storageService.get(Preferences.THEME, StorageScope.GLOBAL, DEFAULT_THEME_ID));
if (newDocument.head.hasChildNodes()) {
newDocument.head.insertBefore(styleElement, newDocument.head.firstChild);
} else {
newDocument.head.appendChild(styleElement);
}
-
- if (newDocument.head.innerHTML !== iFrameDocument.head.innerHTML) {
- iFrameDocument.head.innerHTML = newDocument.head.innerHTML;
- }
- if (newDocument.body.innerHTML !== iFrameDocument.body.innerHTML) {
- iFrameDocument.body.innerHTML = newDocument.body.innerHTML;
+ // 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();
}
@@ -202,78 +235,36 @@ namespace Integration {
'use strict';
- const scriptSource = [
- '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) {', // Listen to keydown events in the iframe
- ' try {',
- ' if (ignoredKeys.some(function(i) { return i === event.keyCode; })) {',
- ' if (!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {',
- ' return;', // we want some single keys to be supported (e.g. Page Down for scrolling)
- ' }',
- ' }',
- '',
- ' if (ignoredCtrlCmdKeys.some(function(i) { return i === event.keyCode; })) {',
- ' if (event.ctrlKey || event.metaKey) {',
- ' return;', // we want some ctrl/cmd keys to be supported (e.g. Ctrl+C for copy)
- ' }',
- ' }',
- '',
- ' if (ignoredShiftKeys.some(function(i) { return i === event.keyCode; })) {',
- ' if (event.shiftKey) {',
- ' return;', // we want some shift keys to be supported (e.g. Shift+Tab for copy)
- ' }',
- ' }',
- '',
- ' event.preventDefault();', // very important to not get duplicate actions when this one bubbles up!
- '',
- ' var fakeEvent = document.createEvent("KeyboardEvent");', // create a keyboard event
- ' Object.defineProperty(fakeEvent, "keyCode", {', // we need to set some properties that Chrome wants
- ' get : function() {',
- ' return event.keyCode;',
- ' }',
- ' });',
- ' Object.defineProperty(fakeEvent, "which", {',
- ' get : function() {',
- ' return event.keyCode;',
- ' }',
- ' });',
- ' Object.defineProperty(fakeEvent, "target", {',
- ' get : function() {',
- ' return window && window.parent.document.body;',
- ' }',
- ' });',
- '',
- ' fakeEvent.initKeyboardEvent("keydown", true, true, document.defaultView, null, null, event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);', // the API shape of this method is not clear to me, but it works ;)
- '',
- ' window.parent.document.dispatchEvent(fakeEvent);', // dispatch the event onto the parent
- ' } catch (error) {}',
- '});',
-
- // disable dropping into iframe!
- 'window.document.addEventListener("dragover", function (e) {',
- ' e.preventDefault();',
- '});',
- 'window.document.addEventListener("drop", function (e) {',
- ' e.preventDefault();',
- '});',
- 'window.document.body.addEventListener("dragover", function (e) {',
- ' e.preventDefault();',
- '});',
- 'window.document.body.addEventListener("drop", function (e) {',
- ' e.preventDefault();',
- '});'
- ];
-
- export function defaultHtml() {
- let all = [
- '',
- ];
- return all.join('\n');
- }
+ // 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 {',
@@ -327,6 +318,7 @@ namespace Integration {
${isLightTheme(themeId)
? defaultLightScrollbarStyle
: defaultDarkScrollbarStyle}`;
+
return styleElement;
}
}