Skip to content

Commit

Permalink
tackle #3089 and #2912
Browse files Browse the repository at this point in the history
  • Loading branch information
jrieken committed Mar 8, 2016
1 parent f1948d6 commit 3e41b2d
Showing 1 changed file with 92 additions and 100 deletions.
192 changes: 92 additions & 100 deletions src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@

'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';
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.
*/
Expand All @@ -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 {
Expand All @@ -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 = <any>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 {
Expand Down Expand Up @@ -109,10 +144,10 @@ export class HtmlPreviewPart extends BaseEditor {
public setVisible(visible: boolean, position?: Position): TPromise<void> {
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();
}
});
}
Expand All @@ -131,7 +166,7 @@ export class HtmlPreviewPart extends BaseEditor {
public setInput(input: EditorInput, options: EditorOptions): TPromise<void> {

this._model = undefined;
this._modelChangeUnbind = cAll(this._modelChangeUnbind);
this._modelChangeSubscription.dispose();
this._lastModelVersion = -1;

if (!(input instanceof HtmlInput)) {
Expand All @@ -147,7 +182,7 @@ export class HtmlPreviewPart extends BaseEditor {
return TPromise.wrapError<void>(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);
Expand All @@ -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 ((<HTMLElement>iFrameDocument.firstChild).innerHTML === '<head></head><body></body>') {
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();
}
Expand All @@ -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 = [
'<html><head></head><body><script>',
...scriptSource,
'</script></body></html>',
];
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 {',
Expand Down Expand Up @@ -327,6 +318,7 @@ namespace Integration {
${isLightTheme(themeId)
? defaultLightScrollbarStyle
: defaultDarkScrollbarStyle}`;

return styleElement;
}
}

0 comments on commit 3e41b2d

Please sign in to comment.