1// /src/shell/types/studio/index.js 2// The Studio, hosted inside an app-shell so it can be a DOCKED TAB on apps.html 3// instead of a page navigation (Jason: "make studio a tab not a new page"). 4// 5// â WHY AN IFRAME, and not the Studio's modules mounted straight into the shell: 6// studio.html's src/app.js owns `document.body` â it builds the canvas, the bottom 7// app-tray, the drop zone, the floating assistant and the welcome splash, all as 8// body-level singletons keyed by fixed element ids. apps.html already has a body of 9// its own (the launcher grids, the fixed dock strip at top:0, its own shells). Two 10// owners of one body is not a layout problem that CSS can settle â it is duplicate 11// ids and duplicate global listeners. A same-origin iframe gives the Studio the 12// whole document it expects, costs one extra JS context, and leaves studio.html 13// working unchanged as a standalone page. 14// 15// Same-origin matters and is deliberate: NO `sandbox` attribute. The Studio needs 16// localStorage (the session token), sessionStorage and its ws-dba WebSocket, and a 17// sandbox without allow-same-origin takes all three away â it would paint a login 18// page inside the tab. This frames OUR OWN page from OUR OWN origin; the untrusted 19// case (a built app's preview) is appviewer/index.js, which IS sandboxed. 20// 21// Tab-switching hides the shell with display:none (shellDock.js). A hidden iframe 22// keeps its document and its socket alive, which is exactly what makes this a tab 23// rather than a bookmark: leave the Studio, come back, the canvas is as you left it. 24 25const STYLE_ID = "zx-studio-shell-styles"; 26 27// The one Studio tab's iframe. There is deliberately only ever one Studio open 28// (appsLauncher.js enforces it): a second would be a second full canvas and a 29// second ws-dba session inside one page. 30let activeFrame = null; 31 32function ensureStyles() { 33 if (document.getElementById(STYLE_ID)) return; 34 const st = document.createElement("style"); 35 st.id = STYLE_ID; 36 st.textContent = ` 37 .zx-studio-wrap { width:100%; height:100%; position:relative; overflow:hidden; background:#111827; } 38 .zx-studio-frame { display:block; width:100%; height:100%; border:none; background:#fff; } 39 /* An iframe eats the pointer stream, so a drag or resize started on the shell 40 chrome dies the moment the cursor crosses into the Studio. shell.js already 41 marks the gesture on the shell element; this is the half that lets go. */ 42 .app-shell.dragging .zx-studio-frame, 43 .app-shell.resizing .zx-studio-frame { pointer-events: none; } 44 .zx-studio-fail { 45 position:absolute; inset:0; display:flex; flex-direction:column; gap:10px; 46 align-items:center; justify-content:center; padding:24px; text-align:center; 47 font:14px/1.5 system-ui; color:#e5e7eb; background:#111827; 48 } 49 .zx-studio-fail a { color:#93c5fd; } 50 `; 51 document.head.appendChild(st); 52} 53 54/** 55 * Resolve the Studio page next to the CURRENT page, not next to this module. 56 * A bare "./studio.html" in an ES module under src/shell/types/studio/ is read 57 * against the document, so it happens to be right today â but only because 58 * apps.html sits in the same directory as studio.html. Deriving it from 59 * location.href says that out loud instead of relying on it. 60 * 61 * @param {{launch?: string, run?: string}} [opts] 62 * @returns {string} 63 */ 64function studioUrl(opts = {}) { 65 const url = new URL("studio.html", window.location.href); 66 // studio.html's autoLaunchFromUrl() reads these: ?run= wins over ?launch=. 67 if (opts.run) url.searchParams.set("run", opts.run); 68 else if (opts.launch) url.searchParams.set("launch", opts.launch); 69 return url.toString(); 70} 71 72/** 73 * Mount the Studio into a shell. 74 * 75 * @param {HTMLElement} el shell.contentArea 76 * @param {HTMLElement} body the wrapper the loader already appended to `el` 77 * @param {object} ctx 78 * @param {object} ctx.shell the shell handle from createShell() 79 * @param {string} [ctx.launch] appId to deep-link open inside the Studio 80 * @param {string} [ctx.run] appbuilder docId to open in the Studio's viewer 81 */ 82export function mount(el, body, { shell, launch, run } = {}) { 83 ensureStyles(); 84 body.classList.add("zx-studio-wrap"); 85 86 const frame = document.createElement("iframe"); 87 frame.className = "zx-studio-frame"; 88 frame.title = "ZIM-X Design Studio"; 89 // The Studio is a full application, not a document: it must be allowed to go 90 // fullscreen and to reach the clipboard for its copy actions. 91 frame.setAttribute("allow", "clipboard-read; clipboard-write; fullscreen"); 92 frame.src = studioUrl({ launch, run }); 93 94 // A blank tab is indistinguishable from a slow one, and the Studio is a heavy 95 // page. Say so if the load never lands rather than leaving an empty rectangle. 96 frame.addEventListener("error", () => { 97 const fail = document.createElement("div"); 98 fail.className = "zx-studio-fail"; 99 fail.innerHTML = 100 "<div><strong>The Studio did not load in this tab.</strong></div>" + 101 `<div>Open it as its own page: <a href="${studioUrl({ launch, run })}">studio.html</a></div>`; 102 body.appendChild(fail); 103 }); 104 105 body.appendChild(frame); 106 107 // The shell's file chrome (save / open / export / NDJSON) is meaningless here â 108 // the Studio owns its own documents â so shell.js leaves it off for this 109 // appType. Nothing else to wire: the iframe IS the content. 110 if (shell?.contentArea) shell.contentArea.style.overflow = "hidden"; 111 112 activeFrame = frame; 113 return { frame }; 114} 115 116/** 117 * Ask the LIVE Studio tab to open an app on its canvas. 118 * 119 * ?launch= only covers the first open â it is read once, at boot. Every later tile 120 * press has to reach a Studio that is already running, and re-pointing the iframe 121 * would reload it and lose the canvas. src/app.js listenForHostLaunch() is the 122 * other end; it checks the origin, so the targetOrigin here is not decorative. 123 * 124 * @param {string} appId a src/appsCatalog.js id 125 * @returns {boolean} false when there is no live Studio frame to talk to 126 */ 127export function postStudioLaunch(appId) { 128 if (!appId) return false;
129 // A closed shell leaves a detached iframe behind, and posting into one is a 130 // silent no-op â exactly the failure that looks like "the tile does nothing". 131 if (!activeFrame || !activeFrame.isConnected || !activeFrame.contentWindow) { 132 activeFrame = null; 133 return false; 134 } 135 activeFrame.contentWindow.postMessage( 136 { type: "zimx:studio-launch", appId }, 137 window.location.origin, 138 ); 139 return true; 140}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.