mirror of
https://github.com/Microsoft/vscode
synced 2024-09-20 02:58:15 +00:00
iFrame work
This commit is contained in:
parent
e3294dc7b1
commit
e1c930e120
|
@ -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('<!DOCTYPE html>');
|
||||
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('<!DOCTYPE html>');
|
||||
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;
|
||||
}
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = createWebviewManager;
|
||||
} else {
|
||||
window.createWebviewManager = createWebviewManager;
|
||||
}
|
||||
}());
|
443
src/vs/workbench/contrib/webview/browser/webviewElement.ts
Normal file
443
src/vs/workbench/contrib/webview/browser/webviewElement.ts
Normal file
|
@ -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<Server> {
|
||||
let address = '';
|
||||
return new Promise<Server>((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<void>;
|
||||
|
||||
private content: WebviewContent;
|
||||
private _focused = false;
|
||||
|
||||
private readonly id: string;
|
||||
private readonly server: Promise<Server>;
|
||||
|
||||
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<void>());
|
||||
public readonly onDidFocus = this._onDidFocus.event;
|
||||
|
||||
private readonly _onDidClickLink = this._register(new Emitter<URI>());
|
||||
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<string | undefined>());
|
||||
public readonly onDidUpdateState = this._onDidUpdateState.event;
|
||||
|
||||
private readonly _onMessage = this._register(new Emitter<any>());
|
||||
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 `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Virtual Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="/main.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
const handlers = {};
|
||||
|
||||
const postMessageToVsCode = (channel, data) => {
|
||||
window.parent.postMessage({ target: '${id}', channel, data }, '*');
|
||||
};
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.origin === '${origin}') {
|
||||
postMessageToVsCode(e.data.command, e.data.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = e.data.channel;
|
||||
const handler = handlers[channel];
|
||||
if (handler) {
|
||||
handler(e, e.data.args);
|
||||
} else {
|
||||
console.log('no handler for ', e);
|
||||
}
|
||||
});
|
||||
|
||||
createWebviewManager({
|
||||
origin: '${origin}',
|
||||
postMessage: (channel, data) => {
|
||||
postMessageToVsCode(channel, data);
|
||||
},
|
||||
onMessage: (channel, handler) => {
|
||||
handlers[channel] = handler;
|
||||
},
|
||||
preProcessHtml: (text) => {
|
||||
return text.replace(/vscode-resource:(?=\\S)/gi, '${SERVER_RESOURCE_ROOT_PATH}');
|
||||
},
|
||||
injectHtml: (newDocument) => {
|
||||
return;
|
||||
const defaultScript = newDocument.createElement('script');
|
||||
defaultScript.textContent = \`
|
||||
(function(){
|
||||
const regexp = new RegExp('^vscode-resource:/', 'g');
|
||||
|
||||
const observer = new MutationObserver(handleMutation);
|
||||
observer.observe(document, { subtree: true, childList: true });
|
||||
|
||||
handleChildNodeMutation(document.head.querySelector('base'));
|
||||
document.head.childNodes.forEach(handleChildNodeMutation);
|
||||
if (document.body) {
|
||||
document.body.childNodes.forEach(handleChildNodeMutation);
|
||||
}
|
||||
|
||||
function handleMutation(records) {
|
||||
for (const record of records) {
|
||||
if (record.target.nodeName === 'HEAD' && record.type === 'childList') {
|
||||
handleChildNodeMutation(document.head.querySelector('base'));
|
||||
record.addedNodes.forEach(handleChildNodeMutation);
|
||||
} else {
|
||||
handleChildNodeMutation(record.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleChildNodeMutation(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (node.nodeName === 'LINK' || node.nodeName === 'BASE') {
|
||||
handleStyleNode(node, 'href');
|
||||
return;
|
||||
}
|
||||
if (node.nodeName === 'SCRIPT') {
|
||||
handleStyleNode(node, 'src');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleStyleNode(target, property) {
|
||||
if (!target[property]) {
|
||||
return;
|
||||
}
|
||||
const match = target[property].match(regexp);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
target.setAttribute(property, target[property].replace(regexp, '${origin}'));
|
||||
}
|
||||
}())
|
||||
\`;
|
||||
|
||||
newDocument.head.prepend(defaultScript);
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
});
|
||||
}());
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue