mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-25 06:10:30 -04:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
132
src/vs/workbench/parts/html/browser/html.contribution.ts
Normal file
132
src/vs/workbench/parts/html/browser/html.contribution.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./media/htmlPreviewPart';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { Position as EditorPosition } from 'vs/platform/editor/common/editor';
|
||||
import { HtmlInput, HtmlInputOptions } from '../common/htmlInput';
|
||||
import { HtmlPreviewPart } from 'vs/workbench/parts/html/browser/htmlPreviewPart';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
|
||||
import { MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
import { WebviewElement } from 'vs/workbench/parts/html/browser/webview';
|
||||
import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions';
|
||||
|
||||
function getActivePreviewsForResource(accessor: ServicesAccessor, resource: URI | string) {
|
||||
const uri = resource instanceof URI ? resource : URI.parse(resource);
|
||||
return accessor.get(IWorkbenchEditorService).getVisibleEditors()
|
||||
.filter(c => c instanceof HtmlPreviewPart && c.model)
|
||||
.map(e => e as HtmlPreviewPart)
|
||||
.filter(e => e.model.uri.scheme === uri.scheme && e.model.uri.fsPath === uri.fsPath);
|
||||
}
|
||||
|
||||
// --- Register Editor
|
||||
(<IEditorRegistry>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
|
||||
|
||||
const defaultPreviewHtmlOptions: HtmlInputOptions = {
|
||||
allowScripts: true,
|
||||
allowSvgs: true
|
||||
};
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.previewHtml', function (
|
||||
accessor: ServicesAccessor,
|
||||
resource: URI | string,
|
||||
position?: EditorPosition,
|
||||
label?: string,
|
||||
options?: HtmlInputOptions
|
||||
) {
|
||||
const uri = resource instanceof URI ? resource : URI.parse(resource);
|
||||
label = label || uri.fsPath;
|
||||
|
||||
let input: HtmlInput;
|
||||
|
||||
// Find already opened HTML input if any
|
||||
const stacks = accessor.get(IEditorGroupService).getStacksModel();
|
||||
const targetGroup = stacks.groupAt(position) || stacks.activeGroup;
|
||||
if (targetGroup) {
|
||||
const existingInput = targetGroup.getEditor(uri);
|
||||
if (existingInput instanceof HtmlInput) {
|
||||
input = existingInput;
|
||||
}
|
||||
}
|
||||
|
||||
const inputOptions = (Object as any).assign({}, options || defaultPreviewHtmlOptions);
|
||||
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
|
||||
inputOptions.svgWhiteList = extensionsWorkbenchService.allowedBadgeProviders;
|
||||
|
||||
// Otherwise, create new input and open it
|
||||
if (!input) {
|
||||
input = accessor.get(IInstantiationService).createInstance(HtmlInput, label, '', uri, inputOptions);
|
||||
} else {
|
||||
input.setName(label); // make sure to use passed in label
|
||||
}
|
||||
|
||||
return accessor.get(IWorkbenchEditorService)
|
||||
.openEditor(input, { pinned: true }, position)
|
||||
.then(editor => true);
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.htmlPreview.postMessage', function (
|
||||
accessor: ServicesAccessor,
|
||||
resource: URI | string,
|
||||
message: any
|
||||
) {
|
||||
const activePreviews = getActivePreviewsForResource(accessor, resource);
|
||||
for (const preview of activePreviews) {
|
||||
preview.sendMessage(message);
|
||||
}
|
||||
return activePreviews.length > 0;
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.htmlPreview.updateOptions', function (
|
||||
accessor: ServicesAccessor,
|
||||
resource: URI | string,
|
||||
options: HtmlInputOptions
|
||||
) {
|
||||
|
||||
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
|
||||
const inputOptions: HtmlInputOptions = options;
|
||||
const allowedBadgeProviders = extensionsWorkbenchService.allowedBadgeProviders;
|
||||
inputOptions.svgWhiteList = allowedBadgeProviders;
|
||||
|
||||
const uri = resource instanceof URI ? resource : URI.parse(resource);
|
||||
const activePreviews = getActivePreviewsForResource(accessor, resource);
|
||||
for (const preview of activePreviews) {
|
||||
if (preview.input && preview.input instanceof HtmlInput) {
|
||||
const input = accessor.get(IInstantiationService).createInstance(HtmlInput, preview.input.getName(), '', uri, options);
|
||||
preview.setInput(input);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_webview.openDevTools', function () {
|
||||
const elements = document.querySelectorAll('webview.ready');
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
try {
|
||||
(elements.item(i) as WebviewElement).openDevTools();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.addCommand({
|
||||
id: '_webview.openDevTools',
|
||||
title: localize('devtools.webview', "Developer: Webview Tools")
|
||||
});
|
||||
236
src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
Normal file
236
src/vs/workbench/parts/html/browser/htmlPreviewPart.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IModel } from 'vs/editor/common/editorCommon';
|
||||
import { Dimension, Builder } from 'vs/base/browser/builder';
|
||||
import { empty as EmptyDisposable, IDisposable, dispose, IReference } from 'vs/base/common/lifecycle';
|
||||
import { EditorOptions, EditorInput } from 'vs/workbench/common/editor';
|
||||
import { Position } from 'vs/platform/editor/common/editor';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
|
||||
import { HtmlInput, HtmlInputOptions, areHtmlInputOptionsEqual } from 'vs/workbench/parts/html/common/htmlInput';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { Parts, IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
import Webview, { WebviewOptions } from './webview';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { WebviewEditor } from './webviewEditor';
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of editor for showing HTML content in an IFrame by leveraging the HTML input.
|
||||
*/
|
||||
export class HtmlPreviewPart extends WebviewEditor {
|
||||
|
||||
static ID: string = 'workbench.editor.htmlPreviewPart';
|
||||
static class: string = 'htmlPreviewPart';
|
||||
|
||||
private _webviewDisposables: IDisposable[];
|
||||
|
||||
private _modelRef: IReference<ITextEditorModel>;
|
||||
public get model(): IModel { return this._modelRef && this._modelRef.object.textEditorModel; }
|
||||
private _modelChangeSubscription = EmptyDisposable;
|
||||
private _themeChangeSubscription = EmptyDisposable;
|
||||
|
||||
private scrollYPercentage: number = 0;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@ITextModelService private textModelResolverService: ITextModelService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
@IPartService private partService: IPartService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(HtmlPreviewPart.ID, telemetryService, themeService, storageService, contextKeyService);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// remove from dom
|
||||
this._webviewDisposables = dispose(this._webviewDisposables);
|
||||
|
||||
// unhook listeners
|
||||
this._themeChangeSubscription.dispose();
|
||||
this._modelChangeSubscription.dispose();
|
||||
|
||||
// dipose model ref
|
||||
dispose(this._modelRef);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
protected createEditor(parent: Builder): void {
|
||||
this.content = document.createElement('div');
|
||||
this.content.style.position = 'absolute';
|
||||
this.content.classList.add(HtmlPreviewPart.class);
|
||||
parent.getHTMLElement().appendChild(this.content);
|
||||
}
|
||||
|
||||
private get webview(): Webview {
|
||||
if (!this._webview) {
|
||||
let webviewOptions: WebviewOptions = {};
|
||||
if (this.input && this.input instanceof HtmlInput) {
|
||||
webviewOptions = this.input.options;
|
||||
}
|
||||
|
||||
this._webview = new Webview(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._contextViewService, this.contextKey, this.findInputFocusContextKey, webviewOptions);
|
||||
if (this.input && this.input instanceof HtmlInput) {
|
||||
const state = this.loadViewState(this.input.getResource());
|
||||
this.scrollYPercentage = state ? state.scrollYPercentage : 0;
|
||||
this.webview.initialScrollProgress = this.scrollYPercentage;
|
||||
|
||||
const resourceUri = this.input.getResource();
|
||||
this.webview.baseUrl = resourceUri.toString(true);
|
||||
}
|
||||
this.onThemeChange(this.themeService.getTheme());
|
||||
this._webviewDisposables = [
|
||||
this._webview,
|
||||
this._webview.onDidClickLink(uri => this.openerService.open(uri)),
|
||||
this._webview.onDidScroll(data => {
|
||||
this.scrollYPercentage = data.scrollYPercentage;
|
||||
}),
|
||||
];
|
||||
}
|
||||
return this._webview;
|
||||
}
|
||||
|
||||
public changePosition(position: Position): void {
|
||||
// what this actually means is that we got reparented. that
|
||||
// has caused the webview to stop working and we need to reset it
|
||||
this._doSetVisible(false);
|
||||
this._doSetVisible(true);
|
||||
|
||||
super.changePosition(position);
|
||||
}
|
||||
|
||||
protected setEditorVisible(visible: boolean, position?: Position): void {
|
||||
this._doSetVisible(visible);
|
||||
super.setEditorVisible(visible, position);
|
||||
}
|
||||
|
||||
private _doSetVisible(visible: boolean): void {
|
||||
if (!visible) {
|
||||
this._themeChangeSubscription.dispose();
|
||||
this._modelChangeSubscription.dispose();
|
||||
this._webviewDisposables = dispose(this._webviewDisposables);
|
||||
this._webview = undefined;
|
||||
} else {
|
||||
this._themeChangeSubscription = this.themeService.onThemeChange(this.onThemeChange.bind(this));
|
||||
|
||||
if (this._hasValidModel()) {
|
||||
this._modelChangeSubscription = this.model.onDidChangeContent(() => this.webview.contents = this.model.getLinesContent());
|
||||
this.webview.contents = this.model.getLinesContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _hasValidModel(): boolean {
|
||||
return this._modelRef && this.model && !this.model.isDisposed();
|
||||
}
|
||||
|
||||
public layout(dimension: Dimension): void {
|
||||
const { width, height } = dimension;
|
||||
this.content.style.width = `${width}px`;
|
||||
this.content.style.height = `${height}px`;
|
||||
if (this._webview) {
|
||||
this._webview.layout();
|
||||
}
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.webview.focus();
|
||||
}
|
||||
|
||||
public clearInput(): void {
|
||||
if (this.input instanceof HtmlInput) {
|
||||
this.saveViewState(this.input.getResource(), {
|
||||
scrollYPercentage: this.scrollYPercentage
|
||||
});
|
||||
}
|
||||
dispose(this._modelRef);
|
||||
this._modelRef = undefined;
|
||||
super.clearInput();
|
||||
}
|
||||
|
||||
public shutdown(): void {
|
||||
if (this.input instanceof HtmlInput) {
|
||||
this.saveViewState(this.input.getResource(), {
|
||||
scrollYPercentage: this.scrollYPercentage
|
||||
});
|
||||
}
|
||||
super.shutdown();
|
||||
}
|
||||
|
||||
public sendMessage(data: any): void {
|
||||
this.webview.sendMessage(data);
|
||||
}
|
||||
|
||||
public setInput(input: EditorInput, options?: EditorOptions): TPromise<void> {
|
||||
|
||||
if (this.input && this.input.matches(input) && this._hasValidModel() && this.input instanceof HtmlInput && input instanceof HtmlInput && areHtmlInputOptionsEqual(this.input.options, input.options)) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
let oldOptions: HtmlInputOptions | undefined = undefined;
|
||||
|
||||
if (this.input instanceof HtmlInput) {
|
||||
oldOptions = this.input.options;
|
||||
this.saveViewState(this.input.getResource(), {
|
||||
scrollYPercentage: this.scrollYPercentage
|
||||
});
|
||||
}
|
||||
|
||||
if (this._modelRef) {
|
||||
this._modelRef.dispose();
|
||||
}
|
||||
this._modelChangeSubscription.dispose();
|
||||
|
||||
if (!(input instanceof HtmlInput)) {
|
||||
return TPromise.wrapError<void>(new Error('Invalid input'));
|
||||
}
|
||||
|
||||
return super.setInput(input, options).then(() => {
|
||||
const resourceUri = input.getResource();
|
||||
return this.textModelResolverService.createModelReference(resourceUri).then(ref => {
|
||||
const model = ref.object;
|
||||
|
||||
if (model instanceof BaseTextEditorModel) {
|
||||
this._modelRef = ref;
|
||||
}
|
||||
|
||||
if (!this.model) {
|
||||
return TPromise.wrapError<void>(new Error(localize('html.voidInput', "Invalid editor input.")));
|
||||
}
|
||||
|
||||
if (oldOptions && !areHtmlInputOptionsEqual(oldOptions, input.options)) {
|
||||
this._doSetVisible(false);
|
||||
}
|
||||
|
||||
this._modelChangeSubscription = this.model.onDidChangeContent(() => {
|
||||
if (this.model) {
|
||||
this.scrollYPercentage = 0;
|
||||
this.webview.contents = this.model.getLinesContent();
|
||||
}
|
||||
});
|
||||
const state = this.loadViewState(resourceUri);
|
||||
this.scrollYPercentage = state ? state.scrollYPercentage : 0;
|
||||
this.webview.baseUrl = resourceUri.toString(true);
|
||||
this.webview.options = input.options;
|
||||
this.webview.contents = this.model.getLinesContent();
|
||||
this.webview.initialScrollProgress = this.scrollYPercentage;
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.htmlPreviewPart {
|
||||
overflow: hidden;
|
||||
}
|
||||
271
src/vs/workbench/parts/html/browser/webview-pre.js
Normal file
271
src/vs/workbench/parts/html/browser/webview-pre.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// @ts-check
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
// state
|
||||
var firstLoad = true;
|
||||
var loadTimeout;
|
||||
var pendingMessages = [];
|
||||
|
||||
const initData = {
|
||||
initialScrollProgress: undefined
|
||||
};
|
||||
|
||||
function styleBody(body) {
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast');
|
||||
body.classList.add(initData.activeTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {HTMLIFrameElement}
|
||||
*/
|
||||
function getActiveFrame() {
|
||||
return document.getElementById('active-frame');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {HTMLIFrameElement}
|
||||
*/
|
||||
function getPendingFrame() {
|
||||
return document.getElementById('pending-frame');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function handleInnerClick(event) {
|
||||
if (!event || !event.view || !event.view.document) {
|
||||
return;
|
||||
}
|
||||
/** @type {any} */
|
||||
var node = event.target;
|
||||
while (node) {
|
||||
if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) {
|
||||
var baseElement = event.view.document.getElementsByTagName("base")[0];
|
||||
if (node.getAttribute("href") === "#") {
|
||||
event.view.scrollTo(0, 0);
|
||||
} else if (node.hash && (node.getAttribute("href") === node.hash || (baseElement && node.href.indexOf(baseElement.href) >= 0))) {
|
||||
var scrollTarget = event.view.document.getElementById(node.hash.substr(1, node.hash.length - 1));
|
||||
if (scrollTarget) {
|
||||
scrollTarget.scrollIntoView();
|
||||
}
|
||||
} else {
|
||||
ipcRenderer.sendToHost("did-click-link", node.href);
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
var isHandlingScroll = false;
|
||||
function handleInnerScroll(event) {
|
||||
if (isHandlingScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = event.target.body.scrollTop / event.target.body.clientHeight;
|
||||
if (isNaN(progress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHandlingScroll = true;
|
||||
window.requestAnimationFrame(function () {
|
||||
try {
|
||||
ipcRenderer.sendToHost('did-scroll', progress);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
isHandlingScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
ipcRenderer.on('baseUrl', function (event, value) {
|
||||
initData.baseUrl = value;
|
||||
});
|
||||
|
||||
ipcRenderer.on('styles', function (event, value, activeTheme) {
|
||||
initData.styles = value;
|
||||
initData.activeTheme = activeTheme;
|
||||
|
||||
// webview
|
||||
var target = getActiveFrame();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
var body = target.contentDocument.getElementsByTagName('body');
|
||||
styleBody(body[0]);
|
||||
|
||||
// iframe
|
||||
var defaultStyles = target.contentDocument.getElementById('_defaultStyles');
|
||||
if (defaultStyles) {
|
||||
defaultStyles.innerHTML = initData.styles;
|
||||
}
|
||||
});
|
||||
|
||||
// propagate focus
|
||||
ipcRenderer.on('focus', function () {
|
||||
const target = getActiveFrame();
|
||||
if (target) {
|
||||
target.contentWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// update iframe-contents
|
||||
ipcRenderer.on('content', function (_event, data) {
|
||||
const options = data.options;
|
||||
const text = data.contents.join('\n');
|
||||
const newDocument = new DOMParser().parseFromString(text, 'text/html');
|
||||
|
||||
// set base-url if applicable
|
||||
if (initData.baseUrl && newDocument.head.getElementsByTagName('base').length === 0) {
|
||||
const baseElement = newDocument.createElement('base');
|
||||
baseElement.href = initData.baseUrl;
|
||||
newDocument.head.appendChild(baseElement);
|
||||
}
|
||||
|
||||
// apply default styles
|
||||
const defaultStyles = newDocument.createElement('style');
|
||||
defaultStyles.id = '_defaultStyles';
|
||||
defaultStyles.innerHTML = initData.styles;
|
||||
if (newDocument.head.hasChildNodes()) {
|
||||
newDocument.head.insertBefore(defaultStyles, newDocument.head.firstChild);
|
||||
} else {
|
||||
newDocument.head.appendChild(defaultStyles);
|
||||
}
|
||||
|
||||
styleBody(newDocument.body);
|
||||
|
||||
const frame = getActiveFrame();
|
||||
|
||||
// keep current scrollTop around and use later
|
||||
var setInitialScrollPosition;
|
||||
if (firstLoad) {
|
||||
firstLoad = false;
|
||||
setInitialScrollPosition = function (body) {
|
||||
if (!isNaN(initData.initialScrollProgress)) {
|
||||
if (body.scrollTop === 0) {
|
||||
body.scrollTop = body.clientHeight * initData.initialScrollProgress;
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentDocument.body.scrollTop : 0;
|
||||
setInitialScrollPosition = function (body) {
|
||||
if (body.scrollTop === 0) {
|
||||
body.scrollTop = scrollY;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up old pending frames and set current one as new one
|
||||
const previousPendingFrame = getPendingFrame();
|
||||
if (previousPendingFrame) {
|
||||
previousPendingFrame.setAttribute('id', '');
|
||||
document.body.removeChild(previousPendingFrame);
|
||||
}
|
||||
pendingMessages = [];
|
||||
|
||||
const newFrame = document.createElement('iframe');
|
||||
newFrame.setAttribute('id', 'pending-frame');
|
||||
newFrame.setAttribute('frameborder', '0');
|
||||
newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin' : 'allow-same-origin');
|
||||
newFrame.style.cssText = "display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden";
|
||||
document.body.appendChild(newFrame);
|
||||
|
||||
// write new content onto iframe
|
||||
newFrame.contentDocument.open('text/html', 'replace');
|
||||
newFrame.contentWindow.onbeforeunload = function () {
|
||||
console.log('prevented webview navigation');
|
||||
return false;
|
||||
};
|
||||
|
||||
var onLoad = function (contentDocument, contentWindow) {
|
||||
if (contentDocument.body) {
|
||||
// Workaround for https://github.com/Microsoft/vscode/issues/12865
|
||||
// check new scrollTop and reset if neccessary
|
||||
setInitialScrollPosition(contentDocument.body);
|
||||
|
||||
// Bubble out link clicks
|
||||
contentDocument.body.addEventListener('click', handleInnerClick);
|
||||
}
|
||||
|
||||
const newFrame = getPendingFrame();
|
||||
if (newFrame && newFrame.contentDocument === contentDocument) {
|
||||
const oldActiveFrame = getActiveFrame();
|
||||
if (oldActiveFrame) {
|
||||
document.body.removeChild(oldActiveFrame);
|
||||
}
|
||||
newFrame.setAttribute('id', 'active-frame');
|
||||
newFrame.style.visibility = 'visible';
|
||||
contentWindow.addEventListener('scroll', handleInnerScroll);
|
||||
|
||||
pendingMessages.forEach(function(data) {
|
||||
contentWindow.postMessage(data, document.location.origin);
|
||||
});
|
||||
pendingMessages = [];
|
||||
}
|
||||
};
|
||||
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout = undefined;
|
||||
loadTimeout = setTimeout(function () {
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout = undefined;
|
||||
onLoad(newFrame.contentDocument, newFrame.contentWindow);
|
||||
}, 200);
|
||||
|
||||
newFrame.contentWindow.addEventListener('load', function (e) {
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout = undefined;
|
||||
onLoad(e.target, this);
|
||||
}
|
||||
});
|
||||
|
||||
// set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off
|
||||
// and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden
|
||||
newFrame.contentDocument.write('<!DOCTYPE html>');
|
||||
newFrame.contentDocument.write(newDocument.documentElement.innerHTML);
|
||||
newFrame.contentDocument.close();
|
||||
|
||||
ipcRenderer.sendToHost('did-set-content');
|
||||
});
|
||||
|
||||
// Forward message to the embedded iframe
|
||||
ipcRenderer.on('message', function (event, data) {
|
||||
const pending = getPendingFrame();
|
||||
if (pending) {
|
||||
pendingMessages.push(data);
|
||||
} else {
|
||||
const target = getActiveFrame();
|
||||
if (target) {
|
||||
target.contentWindow.postMessage(data, document.location.origin);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('initial-scroll-position', function (event, progress) {
|
||||
initData.initialScrollProgress = progress;
|
||||
});
|
||||
|
||||
// forward messages from the embedded iframe
|
||||
window.onmessage = function (message) {
|
||||
ipcRenderer.sendToHost(message.data.command, message.data.data);
|
||||
};
|
||||
|
||||
// signal ready
|
||||
ipcRenderer.sendToHost('webview-ready', process.pid);
|
||||
});
|
||||
}());
|
||||
8
src/vs/workbench/parts/html/browser/webview.html
Normal file
8
src/vs/workbench/parts/html/browser/webview.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="width: 100%; height: 100%">
|
||||
<head>
|
||||
<title>Virtual Document</title>
|
||||
</head>
|
||||
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%">
|
||||
</body>
|
||||
</html>
|
||||
466
src/vs/workbench/parts/html/browser/webview.ts
Normal file
466
src/vs/workbench/parts/html/browser/webview.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { addDisposableListener, addClass } from 'vs/base/browser/dom';
|
||||
import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ITheme, LIGHT, DARK } from 'vs/platform/theme/common/themeService';
|
||||
import { WebviewFindWidget } from './webviewFindWidget';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
export declare interface WebviewElement extends HTMLElement {
|
||||
src: string;
|
||||
preload: string;
|
||||
send(channel: string, ...args: any[]);
|
||||
openDevTools(): any;
|
||||
getWebContents(): any;
|
||||
findInPage(value: string, options?: WebviewElementFindInPageOptions);
|
||||
stopFindInPage(action: string);
|
||||
}
|
||||
|
||||
export class StopFindInPageActions {
|
||||
static clearSelection = 'clearSelection';
|
||||
static keepSelection = 'keepSelection';
|
||||
static activateSelection = 'activateSelection';
|
||||
}
|
||||
|
||||
export interface WebviewElementFindInPageOptions {
|
||||
forward?: boolean;
|
||||
findNext?: boolean;
|
||||
matchCase?: boolean;
|
||||
wordStart?: boolean;
|
||||
medialCapitalAsWordStart?: boolean;
|
||||
}
|
||||
|
||||
export interface FoundInPageResults {
|
||||
requestId: number;
|
||||
activeMatchOrdinal: number;
|
||||
matches: number;
|
||||
selectionArea: any;
|
||||
}
|
||||
|
||||
type ApiThemeClassName = 'vscode-light' | 'vscode-dark' | 'vscode-high-contrast';
|
||||
|
||||
export interface WebviewOptions {
|
||||
allowScripts?: boolean;
|
||||
allowSvgs?: boolean;
|
||||
svgWhiteList?: string[];
|
||||
}
|
||||
|
||||
export default class Webview {
|
||||
private static index: number = 0;
|
||||
|
||||
private _webview: WebviewElement;
|
||||
private _ready: TPromise<this>;
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _onDidClickLink = new Emitter<URI>();
|
||||
|
||||
private _onDidScroll = new Emitter<{ scrollYPercentage: number }>();
|
||||
private _onFoundInPageResults = new Emitter<FoundInPageResults>();
|
||||
|
||||
private _webviewFindWidget: WebviewFindWidget;
|
||||
private _findStarted: boolean = false;
|
||||
|
||||
constructor(
|
||||
private parent: HTMLElement,
|
||||
private _styleElement: Element,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
private _contextKey: IContextKey<boolean>,
|
||||
private _findInputContextKey: IContextKey<boolean>,
|
||||
private _options: WebviewOptions = {},
|
||||
) {
|
||||
this._webview = <any>document.createElement('webview');
|
||||
this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Webview.index++}`);
|
||||
|
||||
// disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick)
|
||||
this._webview.setAttribute('disableblinkfeatures', 'Auxclick');
|
||||
|
||||
this._webview.setAttribute('disableguestresize', '');
|
||||
this._webview.setAttribute('webpreferences', 'contextIsolation=yes');
|
||||
|
||||
this._webview.style.flex = '0 1';
|
||||
this._webview.style.width = '0';
|
||||
this._webview.style.height = '0';
|
||||
this._webview.style.outline = '0';
|
||||
|
||||
this._webview.preload = require.toUrl('./webview-pre.js');
|
||||
this._webview.src = require.toUrl('./webview.html');
|
||||
|
||||
this._ready = new TPromise<this>(resolve => {
|
||||
const subscription = addDisposableListener(this._webview, 'ipc-message', (event) => {
|
||||
if (event.channel === 'webview-ready') {
|
||||
// console.info('[PID Webview] ' event.args[0]);
|
||||
addClass(this._webview, 'ready'); // can be found by debug command
|
||||
|
||||
subscription.dispose();
|
||||
resolve(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!this._options.allowSvgs) {
|
||||
let loaded = false;
|
||||
const subscription = addDisposableListener(this._webview, 'did-start-loading', () => {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
loaded = true;
|
||||
|
||||
const contents = this._webview.getWebContents();
|
||||
if (!contents) {
|
||||
return;
|
||||
}
|
||||
|
||||
contents.session.webRequest.onBeforeRequest((details, callback) => {
|
||||
if (details.url.indexOf('.svg') > 0) {
|
||||
const uri = URI.parse(details.url);
|
||||
if (uri && !uri.scheme.match(/file/i) && (uri.path as any).endsWith('.svg') && !this.isAllowedSvg(uri)) {
|
||||
this.onDidBlockSvg();
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
return callback({});
|
||||
});
|
||||
|
||||
contents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const contentType: string[] = (details.responseHeaders['content-type'] || details.responseHeaders['Content-Type']) as any;
|
||||
if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) {
|
||||
const uri = URI.parse(details.url);
|
||||
if (uri && !this.isAllowedSvg(uri)) {
|
||||
this.onDidBlockSvg();
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
return callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
});
|
||||
});
|
||||
|
||||
this._disposables.push(subscription);
|
||||
}
|
||||
|
||||
this._disposables.push(
|
||||
addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) {
|
||||
console.log(`[Embedded Page] ${e.message}`);
|
||||
}),
|
||||
addDisposableListener(this._webview, 'dom-ready', () => {
|
||||
this.layout();
|
||||
}),
|
||||
addDisposableListener(this._webview, 'crashed', () => {
|
||||
console.error('embedded page crashed');
|
||||
}),
|
||||
addDisposableListener(this._webview, 'ipc-message', (event) => {
|
||||
if (event.channel === 'did-click-link') {
|
||||
let [uri] = event.args;
|
||||
this._onDidClickLink.fire(URI.parse(uri));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.channel === 'did-set-content') {
|
||||
this._webview.style.flex = '';
|
||||
this._webview.style.width = '100%';
|
||||
this._webview.style.height = '100%';
|
||||
this.layout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.channel === 'did-scroll') {
|
||||
if (event.args && typeof event.args[0] === 'number') {
|
||||
this._onDidScroll.fire({ scrollYPercentage: event.args[0] });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}),
|
||||
addDisposableListener(this._webview, 'focus', () => {
|
||||
if (this._contextKey) {
|
||||
this._contextKey.set(true);
|
||||
}
|
||||
}),
|
||||
addDisposableListener(this._webview, 'blur', () => {
|
||||
if (this._contextKey) {
|
||||
this._contextKey.reset();
|
||||
}
|
||||
}),
|
||||
addDisposableListener(this._webview, 'found-in-page', (event) => {
|
||||
this._onFoundInPageResults.fire(event.result);
|
||||
})
|
||||
);
|
||||
|
||||
this._webviewFindWidget = new WebviewFindWidget(this._contextViewService, this);
|
||||
this._disposables.push(this._webviewFindWidget);
|
||||
|
||||
if (parent) {
|
||||
parent.appendChild(this._webviewFindWidget.getDomNode());
|
||||
parent.appendChild(this._webview);
|
||||
}
|
||||
}
|
||||
|
||||
public notifyFindWidgetFocusChanged(isFocused: boolean) {
|
||||
this._contextKey.set(isFocused || document.activeElement === this._webview);
|
||||
}
|
||||
|
||||
public notifyFindWidgetInputFocusChanged(isFocused: boolean) {
|
||||
this._findInputContextKey.set(isFocused);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidClickLink.dispose();
|
||||
this._disposables = dispose(this._disposables);
|
||||
|
||||
if (this._webview.parentElement) {
|
||||
this._webview.parentElement.removeChild(this._webview);
|
||||
const findWidgetDomNode = this._webviewFindWidget.getDomNode();
|
||||
findWidgetDomNode.parentElement.removeChild(findWidgetDomNode);
|
||||
}
|
||||
}
|
||||
|
||||
get onDidClickLink(): Event<URI> {
|
||||
return this._onDidClickLink.event;
|
||||
}
|
||||
|
||||
get onDidScroll(): Event<{ scrollYPercentage: number }> {
|
||||
return this._onDidScroll.event;
|
||||
}
|
||||
|
||||
get onFindResults(): Event<FoundInPageResults> {
|
||||
return this._onFoundInPageResults.event;
|
||||
}
|
||||
|
||||
private _send(channel: string, ...args: any[]): void {
|
||||
this._ready
|
||||
.then(() => this._webview.send(channel, ...args))
|
||||
.done(void 0, console.error);
|
||||
}
|
||||
|
||||
set initialScrollProgress(value: number) {
|
||||
this._send('initial-scroll-position', value);
|
||||
}
|
||||
|
||||
set options(value: WebviewOptions) {
|
||||
this._options = value;
|
||||
}
|
||||
|
||||
set contents(value: string[]) {
|
||||
this._send('content', {
|
||||
contents: value,
|
||||
options: this._options
|
||||
});
|
||||
}
|
||||
|
||||
set baseUrl(value: string) {
|
||||
this._send('baseUrl', value);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._webview.focus();
|
||||
this._send('focus');
|
||||
}
|
||||
|
||||
public sendMessage(data: any): void {
|
||||
this._send('message', data);
|
||||
}
|
||||
|
||||
private onDidBlockSvg() {
|
||||
this.sendMessage({
|
||||
name: 'vscode-did-block-svg'
|
||||
});
|
||||
}
|
||||
|
||||
style(theme: ITheme): void {
|
||||
const { fontFamily, fontWeight, fontSize } = window.getComputedStyle(this._styleElement); // TODO@theme avoid styleElement
|
||||
|
||||
let value = `
|
||||
:root {
|
||||
--background-color: ${theme.getColor(editorBackground)};
|
||||
--color: ${theme.getColor(editorForeground)};
|
||||
--font-family: ${fontFamily};
|
||||
--font-weight: ${fontWeight};
|
||||
--font-size: ${fontSize};
|
||||
}
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--color);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight);
|
||||
font-size: var(--font-size);
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
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: 10px;
|
||||
height: 10px;
|
||||
}`;
|
||||
|
||||
|
||||
let activeTheme: ApiThemeClassName;
|
||||
|
||||
if (theme.type === LIGHT) {
|
||||
value += `
|
||||
::-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);
|
||||
}`;
|
||||
|
||||
activeTheme = 'vscode-light';
|
||||
|
||||
} else if (theme.type === DARK) {
|
||||
value += `
|
||||
::-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);
|
||||
}`;
|
||||
|
||||
activeTheme = 'vscode-dark';
|
||||
|
||||
} else {
|
||||
value += `
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(111, 195, 223, 0.3);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(111, 195, 223, 0.8);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background-color: rgba(111, 195, 223, 0.8);
|
||||
}`;
|
||||
|
||||
activeTheme = 'vscode-high-contrast';
|
||||
}
|
||||
|
||||
this._send('styles', value, activeTheme);
|
||||
|
||||
this._webviewFindWidget.updateTheme(theme);
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
const contents = (this._webview as any).getWebContents();
|
||||
if (!contents || contents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
const window = contents.getOwnerBrowserWindow();
|
||||
if (!window || !window.webContents || window.webContents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
window.webContents.getZoomFactor(factor => {
|
||||
if (contents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
contents.setZoomFactor(factor);
|
||||
|
||||
const width = this.parent.clientWidth;
|
||||
const height = this.parent.clientHeight;
|
||||
contents.setSize({
|
||||
normal: {
|
||||
width: Math.floor(width * factor),
|
||||
height: Math.floor(height * factor)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private isAllowedSvg(uri: URI): boolean {
|
||||
if (this._options.allowSvgs) {
|
||||
return true;
|
||||
}
|
||||
if (this._options.svgWhiteList) {
|
||||
return this._options.svgWhiteList.indexOf(uri.authority.toLowerCase()) >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public startFind(value: string, options?: WebviewElementFindInPageOptions) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure options is defined without modifying the original
|
||||
options = options || {};
|
||||
|
||||
// FindNext must be false for a first request
|
||||
const findOptions: WebviewElementFindInPageOptions = {
|
||||
forward: options.forward,
|
||||
findNext: false,
|
||||
matchCase: options.matchCase,
|
||||
medialCapitalAsWordStart: options.medialCapitalAsWordStart
|
||||
};
|
||||
|
||||
this._findStarted = true;
|
||||
this._webview.findInPage(value, findOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webviews expose a stateful find API.
|
||||
* Successive calls to find will move forward or backward through onFindResults
|
||||
* depending on the supplied options.
|
||||
*
|
||||
* @param {string} value The string to search for. Empty strings are ignored.
|
||||
* @param {WebviewElementFindInPageOptions} [options]
|
||||
*
|
||||
* @memberOf Webview
|
||||
*/
|
||||
public find(value: string, options?: WebviewElementFindInPageOptions): void {
|
||||
// Searching with an empty value will throw an exception
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._findStarted) {
|
||||
this.startFind(value, options);
|
||||
return;
|
||||
}
|
||||
|
||||
this._webview.findInPage(value, options);
|
||||
}
|
||||
|
||||
public stopFind(keepSelection?: boolean): void {
|
||||
this._findStarted = false;
|
||||
this._webview.stopFindInPage(keepSelection ? StopFindInPageActions.keepSelection : StopFindInPageActions.clearSelection);
|
||||
}
|
||||
|
||||
public showFind() {
|
||||
this._webviewFindWidget.reveal();
|
||||
}
|
||||
|
||||
public hideFind() {
|
||||
this._webviewFindWidget.hide();
|
||||
}
|
||||
|
||||
public showNextFindTerm() {
|
||||
this._webviewFindWidget.showNextFindTerm();
|
||||
}
|
||||
|
||||
public showPreviousFindTerm() {
|
||||
this._webviewFindWidget.showPreviousFindTerm();
|
||||
}
|
||||
}
|
||||
188
src/vs/workbench/parts/html/browser/webviewEditor.ts
Normal file
188
src/vs/workbench/parts/html/browser/webviewEditor.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { BaseWebviewEditor } from 'vs/workbench/browser/parts/editor/webviewEditor';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Command, ICommandOptions } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ContextKeyExpr, IContextKey, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
|
||||
import WebView from './webview';
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
|
||||
export interface HtmlPreviewEditorViewState {
|
||||
scrollYPercentage: number;
|
||||
}
|
||||
|
||||
/** A context key that is set when a webview editor has focus. */
|
||||
export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS = new RawContextKey<boolean>('webviewEditorFocus', undefined);
|
||||
/** A context key that is set when a webview editor does not have focus. */
|
||||
export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.toNegated();
|
||||
/** A context key that is set when the find widget find input in webview editor webview is focused. */
|
||||
export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey<boolean>('webviewEditorFindWidgetInputFocused', false);
|
||||
/** A context key that is set when the find widget find input in webview editor webview is not focused. */
|
||||
export const KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.toNegated();
|
||||
|
||||
/**
|
||||
* This class is only intended to be subclassed and not instantiated.
|
||||
*/
|
||||
export abstract class WebviewEditor extends BaseWebviewEditor {
|
||||
|
||||
protected _webviewFocusContextKey: IContextKey<boolean>;
|
||||
protected _webview: WebView;
|
||||
protected content: HTMLElement;
|
||||
protected contextKey: IContextKey<boolean>;
|
||||
protected findInputFocusContextKey: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
telemetryService: ITelemetryService,
|
||||
themeService: IThemeService,
|
||||
storageService: IStorageService,
|
||||
contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super(id, telemetryService, themeService, storageService);
|
||||
if (contextKeyService) {
|
||||
this.contextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS.bindTo(contextKeyService);
|
||||
this.findInputFocusContextKey = KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED.bindTo(contextKeyService);
|
||||
}
|
||||
}
|
||||
|
||||
public showFind() {
|
||||
if (this._webview) {
|
||||
this._webview.showFind();
|
||||
}
|
||||
}
|
||||
|
||||
public hideFind() {
|
||||
if (this._webview) {
|
||||
this._webview.hideFind();
|
||||
}
|
||||
}
|
||||
|
||||
public showNextFindTerm() {
|
||||
if (this._webview) {
|
||||
this._webview.showNextFindTerm();
|
||||
}
|
||||
}
|
||||
|
||||
public showPreviousFindTerm() {
|
||||
if (this._webview) {
|
||||
this._webview.showPreviousFindTerm();
|
||||
}
|
||||
}
|
||||
|
||||
public updateStyles() {
|
||||
super.updateStyles();
|
||||
if (this._webview) {
|
||||
this._webview.style(this.themeService.getTheme());
|
||||
}
|
||||
}
|
||||
|
||||
public get isWebviewEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract createEditor(parent: Builder);
|
||||
}
|
||||
|
||||
class ShowWebViewEditorFindCommand extends Command {
|
||||
public runCommand(accessor: ServicesAccessor, args: any): void {
|
||||
const webViewEditor = this.getWebViewEditor(accessor);
|
||||
if (webViewEditor) {
|
||||
webViewEditor.showFind();
|
||||
}
|
||||
}
|
||||
|
||||
private getWebViewEditor(accessor: ServicesAccessor): WebviewEditor {
|
||||
const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor() as WebviewEditor;
|
||||
if (activeEditor.isWebviewEditor) {
|
||||
return activeEditor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const showFindCommand = new ShowWebViewEditorFindCommand({
|
||||
id: 'editor.action.webvieweditor.showFind',
|
||||
precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS,
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_F
|
||||
}
|
||||
});
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule(showFindCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib()));
|
||||
|
||||
class HideWebViewEditorFindCommand extends Command {
|
||||
public runCommand(accessor: ServicesAccessor, args: any): void {
|
||||
const webViewEditor = this.getWebViewEditor(accessor);
|
||||
if (webViewEditor) {
|
||||
webViewEditor.hideFind();
|
||||
}
|
||||
}
|
||||
|
||||
private getWebViewEditor(accessor: ServicesAccessor): WebviewEditor {
|
||||
const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor() as WebviewEditor;
|
||||
if (activeEditor.isWebviewEditor) {
|
||||
return activeEditor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const hideCommand = new HideWebViewEditorFindCommand({
|
||||
id: 'editor.action.webvieweditor.hideFind',
|
||||
precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FOCUS,
|
||||
kbOpts: {
|
||||
primary: KeyCode.Escape
|
||||
}
|
||||
});
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule(hideCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib()));
|
||||
|
||||
class ShowWebViewEditorFindTermCommand extends Command {
|
||||
constructor(opts: ICommandOptions, private _next: boolean) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
public runCommand(accessor: ServicesAccessor, args: any): void {
|
||||
const webViewEditor = this.getWebViewEditor(accessor);
|
||||
if (webViewEditor) {
|
||||
if (this._next) {
|
||||
webViewEditor.showNextFindTerm();
|
||||
} else {
|
||||
webViewEditor.showPreviousFindTerm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getWebViewEditor(accessor: ServicesAccessor): WebviewEditor {
|
||||
const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor() as WebviewEditor;
|
||||
if (activeEditor.isWebviewEditor) {
|
||||
return activeEditor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const showNextFindTermCommand = new ShowWebViewEditorFindTermCommand({
|
||||
id: 'editor.action.webvieweditor.showNextFindTerm',
|
||||
precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED,
|
||||
kbOpts: {
|
||||
primary: KeyMod.Alt | KeyCode.DownArrow
|
||||
}
|
||||
}, true);
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule(showNextFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib()));
|
||||
|
||||
const showPreviousFindTermCommand = new ShowWebViewEditorFindTermCommand({
|
||||
id: 'editor.action.webvieweditor.showPreviousFindTerm',
|
||||
precondition: KEYBINDING_CONTEXT_WEBVIEWEDITOR_FIND_WIDGET_INPUT_FOCUSED,
|
||||
kbOpts: {
|
||||
primary: KeyMod.Alt | KeyCode.UpArrow
|
||||
}
|
||||
}, false);
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule(showPreviousFindTermCommand.toCommandAndKeybindingRule(KeybindingsRegistry.WEIGHT.editorContrib()));
|
||||
64
src/vs/workbench/parts/html/browser/webviewFindWidget.ts
Normal file
64
src/vs/workbench/parts/html/browser/webviewFindWidget.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SimpleFindWidget } from 'vs/editor/contrib/find/browser/simpleFindWidget';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import Webview from './webview';
|
||||
|
||||
export class WebviewFindWidget extends SimpleFindWidget {
|
||||
|
||||
constructor(
|
||||
@IContextViewService _contextViewService: IContextViewService,
|
||||
private webview: Webview
|
||||
) {
|
||||
super(_contextViewService);
|
||||
|
||||
this.find = this.find.bind(this);
|
||||
this.hide = this.hide.bind(this);
|
||||
this.onInputChanged = this.onInputChanged.bind(this);
|
||||
}
|
||||
|
||||
public find(previous) {
|
||||
let val = this.inputValue;
|
||||
if (this.webview !== null && val) {
|
||||
this.webview.find(val, { findNext: true, forward: !previous });
|
||||
}
|
||||
};
|
||||
|
||||
public hide() {
|
||||
super.hide();
|
||||
this.webview.stopFind(true);
|
||||
this.webview.focus();
|
||||
}
|
||||
|
||||
public onInputChanged() {
|
||||
if (!this.webview) {
|
||||
return;
|
||||
}
|
||||
|
||||
let val = this.inputValue;
|
||||
if (val) {
|
||||
this.webview.startFind(val);
|
||||
} else {
|
||||
this.webview.stopFind(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected onFocusTrackerFocus() {
|
||||
this.webview.notifyFindWidgetFocusChanged(true);
|
||||
}
|
||||
|
||||
protected onFocusTrackerBlur() {
|
||||
this.webview.notifyFindWidgetFocusChanged(false);
|
||||
}
|
||||
|
||||
protected onFindInputFocusTrackerFocus() {
|
||||
this.webview.notifyFindWidgetInputFocusChanged(true);
|
||||
}
|
||||
|
||||
protected onFindInputFocusTrackerBlur() {
|
||||
this.webview.notifyFindWidgetInputFocusChanged(false);
|
||||
}
|
||||
}
|
||||
32
src/vs/workbench/parts/html/common/htmlInput.ts
Normal file
32
src/vs/workbench/parts/html/common/htmlInput.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
|
||||
|
||||
export interface HtmlInputOptions {
|
||||
allowScripts?: boolean;
|
||||
allowSvgs?: boolean;
|
||||
svgWhiteList?: string[];
|
||||
}
|
||||
|
||||
export function areHtmlInputOptionsEqual(left: HtmlInputOptions, right: HtmlInputOptions) {
|
||||
return left.allowScripts === right.allowScripts && left.allowSvgs === right.allowSvgs;
|
||||
}
|
||||
|
||||
export class HtmlInput extends ResourceEditorInput {
|
||||
constructor(
|
||||
name: string,
|
||||
description: string,
|
||||
resource: URI,
|
||||
public readonly options: HtmlInputOptions,
|
||||
@ITextModelService textModelResolverService: ITextModelService
|
||||
) {
|
||||
super(name, description, resource, textModelResolverService);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user