From e1c930e120cd8525a1063f9310ec16fcfd145152 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 11 Jun 2019 22:38:41 -0700 Subject: [PATCH] iFrame work --- .../contrib/webview/browser/pre/main.js | 657 +++++++++--------- .../contrib/webview/browser/webviewElement.ts | 443 ++++++++++++ .../electron-browser/pre/electron-index.js | 5 + .../electron-browser/webviewService.ts | 5 +- 4 files changed, 778 insertions(+), 332 deletions(-) create mode 100644 src/vs/workbench/contrib/webview/browser/webviewElement.ts diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 85dd7a6dbc4..691168dd20f 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -3,41 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // @ts-check -'use strict'; +(function () { + 'use strict'; -/** - * Use polling to track focus of main webview and iframes within the webview - * - * @param {Object} handlers - * @param {() => void} handlers.onFocus - * @param {() => void} handlers.onBlur - */ -const trackFocus = ({ onFocus, onBlur }) => { - const interval = 50; - let isFocused = document.hasFocus(); - setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); - if (isCurrentlyFocused === isFocused) { - return; - } - isFocused = isCurrentlyFocused; - if (isCurrentlyFocused) { - onFocus(); - } else { - onBlur(); - } - }, interval); -}; + /** + * Use polling to track focus of main webview and iframes within the webview + * + * @param {Object} handlers + * @param {() => void} handlers.onFocus + * @param {() => void} handlers.onBlur + */ + const trackFocus = ({ onFocus, onBlur }) => { + const interval = 50; + let isFocused = document.hasFocus(); + setInterval(() => { + const isCurrentlyFocused = document.hasFocus(); + if (isCurrentlyFocused === isFocused) { + return; + } + isFocused = isCurrentlyFocused; + if (isCurrentlyFocused) { + onFocus(); + } else { + onBlur(); + } + }, interval); + }; -const getActiveFrame = () => { - return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame')); -}; + const getActiveFrame = () => { + return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame')); + }; -const getPendingFrame = () => { - return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); -}; + const getPendingFrame = () => { + return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); + }; -const defaultCssRules = ` + const defaultCssRules = ` body { background-color: var(--vscode-editor-background); color: var(--vscode-editor-foreground); @@ -93,170 +94,166 @@ const defaultCssRules = ` background-color: var(--vscode-scrollbarSlider-activeBackground); }`; -/** - * @typedef {{ - * postMessage: (channel: string, data?: any) => void, - * onMessage: (channel: string, handler: any) => void, - * injectHtml?: (document: HTMLDocument) => void, - * preProcessHtml?: (text: string) => void, - * focusIframeOnCreate?: boolean - * }} HostCommunications - */ - -/** - * @param {HostCommunications} host - */ -function createWebviewManager(host) { - // state - let firstLoad = true; - let loadTimeout; - let pendingMessages = []; - let isInDevelopmentMode = false; - - const initData = { - initialScrollProgress: undefined - }; + /** + * @typedef {{ + * postMessage: (channel: string, data?: any) => void, + * onMessage: (channel: string, handler: any) => void, + * injectHtml?: (document: HTMLDocument) => void, + * preProcessHtml?: (text: string) => void, + * focusIframeOnCreate?: boolean + * }} HostCommunications + */ /** - * @param {HTMLDocument?} document - * @param {HTMLElement?} body + * @param {HostCommunications} host */ - const applyStyles = (document, body) => { - if (!document) { - return; - } + function createWebviewManager(host) { + // state + let firstLoad = true; + let loadTimeout; + let pendingMessages = []; + let isInDevelopmentMode = false; - if (body) { - body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); - body.classList.add(initData.activeTheme); - } + const initData = { + initialScrollProgress: undefined + }; - if (initData.styles) { - for (const variable of Object.keys(initData.styles)) { - document.documentElement.style.setProperty(`--${variable}`, initData.styles[variable]); - } - } - }; - - /** - * @param {MouseEvent} event - */ - const handleInnerClick = (event) => { - if (!event || !event.view || !event.view.document) { - return; - } - - let baseElement = event.view.document.getElementsByTagName('base')[0]; - /** @type {any} */ - let node = event.target; - while (node) { - if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { - 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))) { - let scrollTarget = event.view.document.getElementById(node.hash.substr(1, node.hash.length - 1)); - if (scrollTarget) { - scrollTarget.scrollIntoView(); - } - } else { - host.postMessage('did-click-link', node.href.baseVal || node.href); - } - event.preventDefault(); - break; - } - node = node.parentNode; - } - }; - - /** - * @param {KeyboardEvent} e - */ - const handleInnerKeydown = (e) => { - host.postMessage('did-keydown', { - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat - }); - }; - - const onMessage = (message) => { - host.postMessage(message.data.command, message.data.data); - }; - - let isHandlingScroll = false; - const handleInnerScroll = (event) => { - if (!event.target || !event.target.body) { - return; - } - if (isHandlingScroll) { - return; - } - - const progress = event.currentTarget.scrollY / event.target.body.clientHeight; - if (isNaN(progress)) { - return; - } - - isHandlingScroll = true; - window.requestAnimationFrame(() => { - try { - host.postMessage('did-scroll', progress); - } catch (e) { - // noop - } - isHandlingScroll = false; - }); - }; - - document.addEventListener('DOMContentLoaded', () => { - if (!document.body) { - return; - } - - host.onMessage('styles', (_event, data) => { - initData.styles = data.styles; - initData.activeTheme = data.activeTheme; - - const target = getActiveFrame(); - if (!target) { + /** + * @param {HTMLDocument?} document + * @param {HTMLElement?} body + */ + const applyStyles = (document, body) => { + if (!document) { return; } - if (target.contentDocument) { - applyStyles(target.contentDocument, target.contentDocument.body); + if (body) { + body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); + body.classList.add(initData.activeTheme); } - }); - // propagate focus - host.onMessage('focus', () => { - const target = getActiveFrame(); - if (target) { - target.contentWindow.focus(); + if (initData.styles) { + for (const variable of Object.keys(initData.styles)) { + document.documentElement.style.setProperty(`--${variable}`, initData.styles[variable]); + } } - }); + }; - // update iframe-contents - host.onMessage('content', (_event, data) => { - const options = data.options; + /** + * @param {MouseEvent} event + */ + const handleInnerClick = (event) => { + if (!event || !event.view || !event.view.document) { + return; + } - const text = host.preProcessHtml ? host.preProcessHtml(data.contents) : data.contents; - const newDocument = new DOMParser().parseFromString(text, 'text/html'); + let baseElement = event.view.document.getElementsByTagName('base')[0]; + /** @type {any} */ + let node = event.target; + while (node) { + if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { + 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))) { + let scrollTarget = event.view.document.getElementById(node.hash.substr(1, node.hash.length - 1)); + if (scrollTarget) { + scrollTarget.scrollIntoView(); + } + } else { + host.postMessage('did-click-link', node.href.baseVal || node.href); + } + event.preventDefault(); + break; + } + node = node.parentNode; + } + }; - newDocument.querySelectorAll('a').forEach(a => { - if (!a.title) { - a.title = a.getAttribute('href'); + /** + * @param {KeyboardEvent} e + */ + const handleInnerKeydown = (e) => { + host.postMessage('did-keydown', { + key: e.key, + keyCode: e.keyCode, + code: e.code, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + repeat: e.repeat + }); + }; + + let isHandlingScroll = false; + const handleInnerScroll = (event) => { + if (!event.target || !event.target.body) { + return; + } + if (isHandlingScroll) { + return; + } + + const progress = event.currentTarget.scrollY / event.target.body.clientHeight; + if (isNaN(progress)) { + return; + } + + isHandlingScroll = true; + window.requestAnimationFrame(() => { + try { + host.postMessage('did-scroll', progress); + } catch (e) { + // noop + } + isHandlingScroll = false; + }); + }; + + document.addEventListener('DOMContentLoaded', () => { + if (!document.body) { + return; + } + + host.onMessage('styles', (_event, data) => { + initData.styles = data.styles; + initData.activeTheme = data.activeTheme; + + const target = getActiveFrame(); + if (!target) { + return; + } + + if (target.contentDocument) { + applyStyles(target.contentDocument, target.contentDocument.body); } }); - // apply default script - if (options.allowScripts) { - const defaultScript = newDocument.createElement('script'); - defaultScript.textContent = ` + // propagate focus + host.onMessage('focus', () => { + const target = getActiveFrame(); + if (target) { + target.contentWindow.focus(); + } + }); + + // update iframe-contents + host.onMessage('content', (_event, data) => { + const options = data.options; + + const text = host.preProcessHtml ? host.preProcessHtml(data.contents) : data.contents; + const newDocument = new DOMParser().parseFromString(text, 'text/html'); + + newDocument.querySelectorAll('a').forEach(a => { + if (!a.title) { + a.title = a.getAttribute('href'); + } + }); + + // apply default script + if (options.allowScripts) { + const defaultScript = newDocument.createElement('script'); + defaultScript.textContent = ` const acquireVsCodeApi = (function() { const originalPostMessage = window.parent.postMessage.bind(window.parent); let acquired = false; @@ -288,175 +285,175 @@ function createWebviewManager(host) { delete window.frameElement; `; - newDocument.head.prepend(defaultScript); - } + newDocument.head.prepend(defaultScript); + } - // apply default styles - const defaultStyles = newDocument.createElement('style'); - defaultStyles.id = '_defaultStyles'; - defaultStyles.innerHTML = defaultCssRules; - newDocument.head.prepend(defaultStyles); + // apply default styles + const defaultStyles = newDocument.createElement('style'); + defaultStyles.id = '_defaultStyles'; + defaultStyles.innerHTML = defaultCssRules; + newDocument.head.prepend(defaultStyles); - applyStyles(newDocument, newDocument.body); + applyStyles(newDocument, newDocument.body); - if (host.injectHtml) { - host.injectHtml(newDocument); - } + if (host.injectHtml) { + host.injectHtml(newDocument); + } - const frame = getActiveFrame(); - const wasFirstLoad = firstLoad; - // keep current scrollY around and use later - let setInitialScrollPosition; - if (firstLoad) { - firstLoad = false; - setInitialScrollPosition = (body, window) => { - if (!isNaN(initData.initialScrollProgress)) { - if (window.scrollY === 0) { - window.scroll(0, body.clientHeight * initData.initialScrollProgress); + const frame = getActiveFrame(); + const wasFirstLoad = firstLoad; + // keep current scrollY around and use later + let setInitialScrollPosition; + if (firstLoad) { + firstLoad = false; + setInitialScrollPosition = (body, window) => { + if (!isNaN(initData.initialScrollProgress)) { + if (window.scrollY === 0) { + window.scroll(0, body.clientHeight * initData.initialScrollProgress); + } } - } - }; - } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0; - setInitialScrollPosition = (body, window) => { - if (window.scrollY === 0) { - window.scroll(0, 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); - } - if (!wasFirstLoad) { - 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.addEventListener('keydown', handleInnerKeydown); - - newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { - const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - if (contentDocument) { - applyStyles(contentDocument, contentDocument.body); - } - }); - - newFrame.contentWindow.onbeforeunload = () => { - if (isInDevelopmentMode) { // Allow reloads while developing a webview - host.postMessage('do-reload'); - return false; + }; + } else { + const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0; + setInitialScrollPosition = (body, window) => { + if (window.scrollY === 0) { + window.scroll(0, scrollY); + } + }; } - // Block navigation when not in development mode - console.log('prevented webview navigation'); - return false; - }; - - const onLoad = (contentDocument, contentWindow) => { - if (contentDocument && contentDocument.body) { - // Workaround for https://github.com/Microsoft/vscode/issues/12865 - // check new scrollY and reset if neccessary - setInitialScrollPosition(contentDocument.body, contentWindow); + // Clean up old pending frames and set current one as new one + const previousPendingFrame = getPendingFrame(); + if (previousPendingFrame) { + previousPendingFrame.setAttribute('id', ''); + document.body.removeChild(previousPendingFrame); } - - const newFrame = getPendingFrame(); - if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { - const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } - // Styles may have changed since we created the element. Make sure we re-style - applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); - newFrame.setAttribute('id', 'active-frame'); - newFrame.style.visibility = 'visible'; - if (host.focusIframeOnCreate) { - newFrame.contentWindow.focus(); - } - - contentWindow.addEventListener('scroll', handleInnerScroll); - - pendingMessages.forEach((data) => { - contentWindow.postMessage(data, '*'); - }); + if (!wasFirstLoad) { pendingMessages = []; } - }; - clearTimeout(loadTimeout); - loadTimeout = undefined; - loadTimeout = setTimeout(() => { + 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.addEventListener('keydown', handleInnerKeydown); + + newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { + const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; + if (contentDocument) { + applyStyles(contentDocument, contentDocument.body); + } + }); + + newFrame.contentWindow.onbeforeunload = () => { + if (isInDevelopmentMode) { // Allow reloads while developing a webview + host.postMessage('do-reload'); + return false; + } + + // Block navigation when not in development mode + console.log('prevented webview navigation'); + return false; + }; + + const onLoad = (contentDocument, contentWindow) => { + if (contentDocument && contentDocument.body) { + // Workaround for https://github.com/Microsoft/vscode/issues/12865 + // check new scrollY and reset if neccessary + setInitialScrollPosition(contentDocument.body, contentWindow); + } + + const newFrame = getPendingFrame(); + if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { + const oldActiveFrame = getActiveFrame(); + if (oldActiveFrame) { + document.body.removeChild(oldActiveFrame); + } + // Styles may have changed since we created the element. Make sure we re-style + applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); + newFrame.setAttribute('id', 'active-frame'); + newFrame.style.visibility = 'visible'; + if (host.focusIframeOnCreate) { + newFrame.contentWindow.focus(); + } + + contentWindow.addEventListener('scroll', handleInnerScroll); + + pendingMessages.forEach((data) => { + contentWindow.postMessage(data, '*'); + }); + pendingMessages = []; + } + }; + clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(newFrame.contentDocument, newFrame.contentWindow); - }, 200); - - newFrame.contentWindow.addEventListener('load', function (e) { - if (loadTimeout) { + loadTimeout = setTimeout(() => { clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(e.target, this); - } + onLoad(newFrame.contentDocument, newFrame.contentWindow); + }, 200); + + newFrame.contentWindow.addEventListener('load', function (e) { + if (loadTimeout) { + clearTimeout(loadTimeout); + loadTimeout = undefined; + onLoad(e.target, this); + } + }); + + // Bubble out link clicks + newFrame.contentWindow.addEventListener('click', handleInnerClick); + + // 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(''); + newFrame.contentDocument.write(newDocument.documentElement.innerHTML); + newFrame.contentDocument.close(); + + host.postMessage('did-set-content', undefined); }); - // Bubble out link clicks - newFrame.contentWindow.addEventListener('click', handleInnerClick); - - // 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(''); - newFrame.contentDocument.write(newDocument.documentElement.innerHTML); - newFrame.contentDocument.close(); - - host.postMessage('did-set-content', undefined); - }); - - // Forward message to the embedded iframe - host.onMessage('message', (_event, data) => { - const pending = getPendingFrame(); - if (!pending) { - const target = getActiveFrame(); - if (target) { - target.contentWindow.postMessage(data, '*'); - return; + // Forward message to the embedded iframe + host.onMessage('message', (_event, data) => { + const pending = getPendingFrame(); + if (!pending) { + const target = getActiveFrame(); + if (target) { + target.contentWindow.postMessage(data, '*'); + return; + } } - } - pendingMessages.push(data); + pendingMessages.push(data); + }); + + host.onMessage('initial-scroll-position', (_event, progress) => { + initData.initialScrollProgress = progress; + }); + + host.onMessage('devtools-opened', () => { + isInDevelopmentMode = true; + }); + + trackFocus({ + onFocus: () => host.postMessage('did-focus'), + onBlur: () => host.postMessage('did-blur') + }); + + // signal ready + host.postMessage('webview-ready', {}); }); + } - host.onMessage('initial-scroll-position', (_event, progress) => { - initData.initialScrollProgress = progress; - }); - - host.onMessage('devtools-opened', () => { - isInDevelopmentMode = true; - }); - - trackFocus({ - onFocus: () => host.postMessage('did-focus'), - onBlur: () => host.postMessage('did-blur') - }); - - // Forward messages from the embedded iframe - window.onmessage = onMessage; - - // signal ready - host.postMessage('webview-ready', {}); - }); -} - -if (typeof module !== 'undefined') { - module.exports = createWebviewManager; -} \ No newline at end of file + if (typeof module !== 'undefined') { + module.exports = createWebviewManager; + } else { + window.createWebviewManager = createWebviewManager; + } +}()); \ No newline at end of file diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts new file mode 100644 index 00000000000..fc5486d2b7f --- /dev/null +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; +import { addDisposableListener, addClass } from 'vs/base/browser/dom'; +import { createServer } from 'http'; +import * as fs from 'fs'; +import * as path from 'path'; +import { startsWith } from 'vs/base/common/strings'; +import { getWebviewThemeData } from 'vs/workbench/contrib/webview/common/themeing'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { getWebviewContentMimeType } from 'vs/workbench/contrib/webview/common/mimeTypes'; + +const SERVER_RESOURCE_ROOT_PATH = '/resource/'; + +class Server { + public static make( + id: string, + fileService: IFileService + ): Promise { + let address = ''; + return new Promise((resolve, reject) => { + const server = createServer((req, res) => { + if (req.url === '/') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.write(getHtml(id, address)); + res.end(); + return; + } + if (req.url === '/main.js') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.write(fs.readFileSync(path.join(__dirname.replace('file:', ''), 'pre', 'main.js'))); + res.end(); + return; + } + if (req.url && startsWith(req.url, SERVER_RESOURCE_ROOT_PATH)) { + const path = URI.file(req.url.replace(SERVER_RESOURCE_ROOT_PATH, '/')); + res.writeHead(200, { 'Content-Type': getWebviewContentMimeType(path) }); + fileService.readFile(path).then(result => { + res.write(result.value.buffer); + }).finally(() => { + res.end(); + }); + return; + } + + res.writeHead(404); + res.end(); + return; + }); + + server.on('error', reject); + const l = server.listen(() => { + server.removeListener('error', reject); + address = `http://localhost:${l.address().port}`; + resolve(new Server(server, l.address().port)); + }); + }); + } + + private constructor( + public readonly server: import('http').Server, + public readonly port: number + ) { } + + dispose() { + this.server.close(); + } +} + +interface WebviewContent { + readonly html: string; + readonly options: WebviewContentOptions; + readonly state: string | undefined; +} + +export class IFrameWebview extends Disposable implements Webview { + private element: HTMLIFrameElement; + + private _ready: Promise; + + private content: WebviewContent; + private _focused = false; + + private readonly id: string; + private readonly server: Promise; + + constructor( + private readonly _options: WebviewOptions, + contentOptions: WebviewContentOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + this.content = { + html: '', + options: contentOptions, + state: undefined + }; + + this.id = `webview-${Date.now()}`; + + this.element = document.createElement('iframe'); + this.element.sandbox.add('allow-scripts'); + this.element.sandbox.add('allow-same-origin'); + this.element.setAttribute('src', ''); + this.element.style.border = 'none'; + this.element.style.width = '100%'; + this.element.style.height = '100%'; + + this.server = Server.make(this.id, fileService); + this.server.then(async server => { + this.element.setAttribute('src', `http://localhost:${server.port}`); + }); + + this._register(addDisposableListener(window, 'message', e => { + if (!e || !e.data || e.data.target !== this.id) { + return; + } + + switch (e.data.channel) { + case 'onmessage': + if (e.data.data) { + this._onMessage.fire(e.data.data); + } + return; + + case 'did-click-link': + let [uri] = e.data.data; + this._onDidClickLink.fire(URI.parse(uri)); + return; + + case 'did-set-content': + // this._webview.style.flex = ''; + // this._webview.style.width = '100%'; + // this._webview.style.height = '100%'; + // this.layout(); + return; + + case 'did-scroll': + // if (event.args && typeof event.args[0] === 'number') { + // this._onDidScroll.fire({ scrollYPercentage: event.args[0] }); + // } + return; + + case 'do-reload': + this.reload(); + return; + + case 'do-update-state': + const state = e.data.data; + this.state = state; + this._onDidUpdateState.fire(state); + return; + + case 'did-focus': + this.handleFocusChange(true); + return; + + case 'did-blur': + this.handleFocusChange(false); + return; + + } + })); + + this._ready = new Promise(resolve => { + const subscription = this._register(addDisposableListener(window, 'message', (e) => { + if (e.data && e.data.target === this.id && e.data.channel === 'webview-ready') { + addClass(this.element, 'ready'); + subscription.dispose(); + resolve(); + } + })); + }); + + this.style(themeService.getTheme()); + this._register(themeService.onThemeChange(this.style, this)); + } + + public mountTo(parent: HTMLElement) { + parent.appendChild(this.element); + } + + public set options(options: WebviewContentOptions) { + if (areWebviewInputOptionsEqual(options, this.content.options)) { + return; + } + + this.content = { + html: this.content.html, + options: options, + state: this.content.state, + }; + this.doUpdateContent(); + } + + public set html(value: string) { + this.content = { + html: value, + options: this.content.options, + state: this.content.state, + }; + this.doUpdateContent(); + } + + public update(html: string, options: WebviewContentOptions, retainContextWhenHidden: boolean) { + if (retainContextWhenHidden && html === this.content.html && areWebviewInputOptionsEqual(options, this.content.options)) { + return; + } + this.content = { + html: html, + options: options, + state: this.content.state, + }; + this.doUpdateContent(); + } + + private doUpdateContent() { + this._send('content', { + contents: this.content.html, + options: this.content.options, + state: this.content.state + }); + } + + private handleFocusChange(isFocused: boolean): void { + this._focused = isFocused; + if (isFocused) { + this._onDidFocus.fire(); + } + } + + initialScrollProgress: number; + state: string | undefined; + + private readonly _onDidFocus = this._register(new Emitter()); + public readonly onDidFocus = this._onDidFocus.event; + + private readonly _onDidClickLink = this._register(new Emitter()); + public readonly onDidClickLink = this._onDidClickLink.event; + + private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number }>()); + public readonly onDidScroll = this._onDidScroll.event; + + private readonly _onDidUpdateState = this._register(new Emitter()); + public readonly onDidUpdateState = this._onDidUpdateState.event; + + private readonly _onMessage = this._register(new Emitter()); + public readonly onMessage = this._onMessage.event; + + + sendMessage(data: any): void { + this._send('message', data); + } + + + layout(): void { + // noop + } + + focus(): void { + this.element.focus(); + } + dispose(): void { + if (this.element) { + if (this.element.parentElement) { + this.element.parentElement.removeChild(this.element); + } + } + + this.element = undefined!; + super.dispose(); + } + + reload(): void { + throw new Error('Method not implemented.'); + } + selectAll(): void { + throw new Error('Method not implemented.'); + } + copy(): void { + throw new Error('Method not implemented.'); + } + paste(): void { + throw new Error('Method not implemented.'); + } + cut(): void { + throw new Error('Method not implemented.'); + } + undo(): void { + throw new Error('Method not implemented.'); + } + redo(): void { + throw new Error('Method not implemented.'); + } + showFind(): void { + throw new Error('Method not implemented.'); + } + hideFind(): void { + throw new Error('Method not implemented.'); + } + + private _send(channel: string, data: any): void { + this._ready + .then(() => this.element.contentWindow!.postMessage({ + channel: channel, + args: data + }, '*')) + .catch(err => console.error(err)); + } + + + private style(theme: ITheme): void { + const { styles, activeTheme } = getWebviewThemeData(theme, this._configurationService); + this._send('styles', { styles, activeTheme }); + } +} + +function getHtml(id: string, origin: string): any { + return ` + + + + + + + + Virtual Document + + + + + +`; +} diff --git a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js index 4762aa24c4b..a5f4d3b1809 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js +++ b/src/vs/workbench/contrib/webview/electron-browser/pre/electron-index.js @@ -40,5 +40,10 @@ document.addEventListener('DOMContentLoaded', () => { registerVscodeResourceScheme(); + + // Forward messages from the embedded iframe + window.onmessage = (message) => { + ipcRenderer.sendToHost(message.data.command, message.data.data); + }; }); }()); \ No newline at end of file diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts index e341b1864ea..c158e3212b6 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWebviewService, Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview'; -import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; +// import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; +import { IFrameWebview as WebviewElement } from 'vs/workbench/contrib/webview/browser/webviewElement'; +import { IWebviewService, WebviewOptions, WebviewContentOptions, Webview } from 'vs/workbench/contrib/webview/common/webview'; export class WebviewService implements IWebviewService { _serviceBrand: any;