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
 | ||||
| // (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) { | ||||
|   const url = getIframeUrl(iframePort, iframeUrl); | ||||
| 
 | ||||
|   return verifyIframeSource(url).then(() => { | ||||
|     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 = | ||||
|       "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; | ||||
|  | @ -48,8 +48,8 @@ function getIframeUrl(iframePort, iframeUrl) { | |||
|   } | ||||
| 
 | ||||
|   return protocol === "https:" | ||||
|     ? "https://livebookusercontent.com/iframe/v4.html" | ||||
|     : `http://${window.location.hostname}:${iframePort}/iframe/v4.html`; | ||||
|     ? "https://livebookusercontent.com/iframe/v5.html" | ||||
|     : `http://${window.location.hostname}:${iframePort}/iframe/v5.html`; | ||||
| } | ||||
| 
 | ||||
| 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