mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-06 11:46:30 +08:00
Send link navigation event inside iframe to parent (#2160)
This commit is contained in:
parent
2ae39f59fb
commit
60d9beda5a
2 changed files with 244 additions and 4 deletions
|
|
@ -26,14 +26,14 @@ import { sha256Base64 } from "../../lib/utils";
|
||||||
// (2): https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
|
// (2): https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
|
||||||
// (3): https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
|
// (3): https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
|
||||||
|
|
||||||
const IFRAME_SHA256 = "vd7g1B8fLBFZH6C6KNpG4H8B0SQ/oIuqKaTW6jD053A=";
|
const IFRAME_SHA256 = "48LZtKkFYMd+4gsmVvbhvw9mTpJPw+ItRdGxPPs+5xw=";
|
||||||
|
|
||||||
export function initializeIframeSource(iframe, iframePort, iframeUrl) {
|
export function initializeIframeSource(iframe, iframePort, iframeUrl) {
|
||||||
const url = getIframeUrl(iframePort, iframeUrl);
|
const url = getIframeUrl(iframePort, iframeUrl);
|
||||||
|
|
||||||
return verifyIframeSource(url).then(() => {
|
return verifyIframeSource(url).then(() => {
|
||||||
iframe.sandbox =
|
iframe.sandbox =
|
||||||
"allow-scripts allow-same-origin allow-downloads allow-modals allow-popups";
|
"allow-scripts allow-same-origin allow-downloads allow-modals allow-popups allow-top-navigation";
|
||||||
iframe.allow =
|
iframe.allow =
|
||||||
"accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; usb; xr-spatial-tracking; clipboard-read; clipboard-write";
|
"accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; usb; xr-spatial-tracking; clipboard-read; clipboard-write";
|
||||||
iframe.src = url;
|
iframe.src = url;
|
||||||
|
|
@ -48,8 +48,8 @@ function getIframeUrl(iframePort, iframeUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return protocol === "https:"
|
return protocol === "https:"
|
||||||
? "https://livebookusercontent.com/iframe/v4.html"
|
? "https://livebookusercontent.com/iframe/v5.html"
|
||||||
: `http://${window.location.hostname}:${iframePort}/iframe/v4.html`;
|
: `http://${window.location.hostname}:${iframePort}/iframe/v5.html`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let iframeVerificationPromise = null;
|
let iframeVerificationPromise = null;
|
||||||
|
|
|
||||||
240
iframe/priv/static/iframe/v5.html
Normal file
240
iframe/priv/static/iframe/v5.html
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<base target="_parent">
|
||||||
|
<title>Output</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Invoke the init function in a separate context for better isolation
|
||||||
|
function applyInit(init, ctx, data) {
|
||||||
|
init(ctx, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const state = {
|
||||||
|
token: null,
|
||||||
|
importPromise: null,
|
||||||
|
eventHandlers: {},
|
||||||
|
eventQueue: [],
|
||||||
|
syncHandler: null,
|
||||||
|
secretHandler: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function postMessage(message) {
|
||||||
|
window.parent.postMessage({ token: state.token, ...message }, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
root: document.getElementById("root"),
|
||||||
|
|
||||||
|
handleEvent(event, callback) {
|
||||||
|
if (state.eventHandlers[event]) {
|
||||||
|
throw new Error(
|
||||||
|
`Handler has already been defined for event "${event}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.eventHandlers[event] = callback;
|
||||||
|
|
||||||
|
while (
|
||||||
|
state.eventQueue.length > 0 &&
|
||||||
|
state.eventHandlers[state.eventQueue[0].event]
|
||||||
|
) {
|
||||||
|
const { event, payload } = state.eventQueue.shift();
|
||||||
|
const handler = state.eventHandlers[event];
|
||||||
|
handler(payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
pushEvent(event, payload = null) {
|
||||||
|
postMessage({ type: "event", event, payload });
|
||||||
|
},
|
||||||
|
|
||||||
|
importCSS(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const linkEl = document.createElement("link");
|
||||||
|
linkEl.addEventListener(
|
||||||
|
"load",
|
||||||
|
(event) => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
linkEl.rel = "stylesheet";
|
||||||
|
linkEl.href = url;
|
||||||
|
document.head.appendChild(linkEl);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
importJS(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scriptEl = document.createElement("script");
|
||||||
|
scriptEl.addEventListener(
|
||||||
|
"load",
|
||||||
|
(event) => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
scriptEl.src = url;
|
||||||
|
document.head.appendChild(scriptEl);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSync(callback) {
|
||||||
|
state.syncHandler = callback;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectSecret(callback, preselectName, options = {}) {
|
||||||
|
state.secretHandler = callback;
|
||||||
|
postMessage({ type: "selectSecret", preselectName, options });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.source === window.parent) {
|
||||||
|
handleParentMessage(event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleParentMessage(message) {
|
||||||
|
if (message.type === "readyReply") {
|
||||||
|
state.token = message.token;
|
||||||
|
onReady();
|
||||||
|
|
||||||
|
// Set the base URL for relative URLs
|
||||||
|
const baseUrlEl = document.createElement("base");
|
||||||
|
baseUrlEl.href = message.baseUrl;
|
||||||
|
document.head.appendChild(baseUrlEl);
|
||||||
|
// We already entered the script and the base URL change
|
||||||
|
// doesn't impact this import call, so we use the absolute
|
||||||
|
// URL instead
|
||||||
|
state.importPromise = import(`${message.baseUrl}${message.jsPath}`);
|
||||||
|
} else if (message.type === "init") {
|
||||||
|
state.importPromise
|
||||||
|
.then((module) => {
|
||||||
|
const init = module.init;
|
||||||
|
|
||||||
|
if (!init) {
|
||||||
|
const fns = Object.keys(module);
|
||||||
|
throw new Error(
|
||||||
|
`Expected the module to export an init function, but found: ${fns.join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyInit(init, ctx, message.data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
renderErrorMessage(
|
||||||
|
`Failed to load the widget JS module, got the following error:\n\n ${error.message}\n\nSee the browser console for more details. If running behind an authentication proxy, make sure the /public/* routes are publicly accessible.`
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
} else if (message.type === "event") {
|
||||||
|
const { event, payload } = message;
|
||||||
|
const handler = state.eventHandlers[event];
|
||||||
|
|
||||||
|
if (state.eventQueue.length === 0 && handler) {
|
||||||
|
handler(payload);
|
||||||
|
} else {
|
||||||
|
state.eventQueue.push({ event, payload });
|
||||||
|
}
|
||||||
|
} else if (message.type === "sync") {
|
||||||
|
Promise.resolve(state.syncHandler && state.syncHandler()).then(
|
||||||
|
() => {
|
||||||
|
postMessage({ type: "syncReply" });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (message.type === "secretSelected") {
|
||||||
|
state.secretHandler && state.secretHandler(message.secretName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage({ type: "ready" });
|
||||||
|
|
||||||
|
function onReady() {
|
||||||
|
// Report height changes
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
postMessage({ type: "resize", height: document.body.scrollHeight });
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(document.body);
|
||||||
|
|
||||||
|
// Forward relevant DOM events
|
||||||
|
|
||||||
|
window.addEventListener("mousedown", (event) => {
|
||||||
|
postMessage({ type: "domEvent", event: { type: "mousedown" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("focus", (event) => {
|
||||||
|
postMessage({ type: "domEvent", event: { type: "focus" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
postMessage({
|
||||||
|
type: "domEvent",
|
||||||
|
event: keyboardEventToPayload(event),
|
||||||
|
isTargetEditable: isEditableElement(event.target),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditableElement(element) {
|
||||||
|
return element.matches("input, textarea, [contenteditable]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardEventToPayload(event) {
|
||||||
|
const {
|
||||||
|
altKey,
|
||||||
|
code,
|
||||||
|
ctrlKey,
|
||||||
|
isComposing,
|
||||||
|
key,
|
||||||
|
location,
|
||||||
|
metaKey,
|
||||||
|
repeat,
|
||||||
|
shiftKey,
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: event.type,
|
||||||
|
props: {
|
||||||
|
altKey,
|
||||||
|
code,
|
||||||
|
ctrlKey,
|
||||||
|
isComposing,
|
||||||
|
key,
|
||||||
|
location,
|
||||||
|
metaKey,
|
||||||
|
repeat,
|
||||||
|
shiftKey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderErrorMessage(message) {
|
||||||
|
ctx.root.innerHTML = `
|
||||||
|
<div style="color: #FF3E38; white-space: pre-wrap; word-break: break-word;">${message}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue