mirror of
				https://github.com/livebook-dev/livebook.git
				synced 2025-11-01 00:06:04 +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