sandbox: re-handshake on iframe reload + size to content (fixes invisible UI)
This commit is contained in:
parent
06c3d90da4
commit
1b253a9c0f
4 changed files with 53 additions and 35 deletions
|
|
@ -3,8 +3,9 @@
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<title>Orbit sandboxed plugin</title>
|
<title>Orbit sandboxed plugin</title>
|
||||||
<style>
|
<style>
|
||||||
html, body { margin: 0; background: transparent; color: inherit; font: inherit; }
|
html, body { margin: 0; padding: 0; background: transparent; color: inherit; }
|
||||||
body { font: 13px/1.3 system-ui, sans-serif; }
|
/* inline-block shrink-wraps the body to its content so we can size the iframe to it */
|
||||||
|
body { display: inline-block; font: 13px/1.3 system-ui, sans-serif; }
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -71,9 +72,12 @@
|
||||||
ui: function (slot, build) {
|
ui: function (slot, build) {
|
||||||
try { build(document.body); } catch (e) { rpc('log', 'ui build threw: ' + e); }
|
try { build(document.body); } catch (e) { rpc('log', 'ui build threw: ' + e); }
|
||||||
rpc('ui.claim', slot);
|
rpc('ui.claim', slot);
|
||||||
var report = function () { rpc('ui.resize', document.documentElement.scrollHeight); };
|
var report = function () {
|
||||||
|
var r = document.body.getBoundingClientRect();
|
||||||
|
rpc('ui.resize', Math.ceil(r.width), Math.ceil(r.height));
|
||||||
|
};
|
||||||
report();
|
report();
|
||||||
if (window.ResizeObserver) new ResizeObserver(report).observe(document.documentElement);
|
if (window.ResizeObserver) new ResizeObserver(report).observe(document.body);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ Orbit.plugin('sandbox-demo', function (orbit, log) {
|
||||||
orbit.ui('footer_item', function (root) {
|
orbit.ui('footer_item', function (root) {
|
||||||
var btn = document.createElement('button');
|
var btn = document.createElement('button');
|
||||||
btn.textContent = '🧪 wave';
|
btn.textContent = '🧪 wave';
|
||||||
btn.style.cssText = 'font:inherit;cursor:pointer;border:1px solid #8884;border-radius:8px;' +
|
// Explicit colours: the sandbox is isolated from the app's theme vars, so a
|
||||||
'padding:.25rem .5rem;background:transparent;color:inherit';
|
// self-contained plugin ships its own visible styling (works on light + dark).
|
||||||
|
btn.style.cssText = 'font:600 13px system-ui,sans-serif;cursor:pointer;border:0;border-radius:8px;' +
|
||||||
|
'padding:.3rem .6rem;background:#8957e5;color:#fff;white-space:nowrap';
|
||||||
btn.onclick = function () {
|
btn.onclick = function () {
|
||||||
var n = (orbit.storage.get('waves', 0)) + 1;
|
var n = (orbit.storage.get('waves', 0)) + 1;
|
||||||
orbit.storage.set('waves', n);
|
orbit.storage.set('waves', n);
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,8 @@ input, textarea { font-family: inherit; }
|
||||||
border-radius: 11px; font-size: 1.1rem; color: var(--muted); cursor: pointer; transition: background .12s, color .12s;
|
border-radius: 11px; font-size: 1.1rem; color: var(--muted); cursor: pointer; transition: background .12s, color .12s;
|
||||||
}
|
}
|
||||||
.appbar__act:hover { background: var(--bg); color: var(--ink); }
|
.appbar__act:hover { background: var(--bg); color: var(--ink); }
|
||||||
|
/* host mount point for a sandboxed plugin's iframe (sized to content by the host) */
|
||||||
|
.sbx-slot { display: inline-flex; align-items: center; }
|
||||||
/* away toggle: dim = present (click to step away), amber-lit = away */
|
/* away toggle: dim = present (click to step away), amber-lit = away */
|
||||||
.appbar__act--away { filter: grayscale(1); opacity: .55; }
|
.appbar__act--away { filter: grayscale(1); opacity: .55; }
|
||||||
.appbar__act--away:hover { filter: grayscale(.5); opacity: .85; }
|
.appbar__act--away:hover { filter: grayscale(.5); opacity: .85; }
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,8 @@ export async function mountSandboxedPlugin(entry: Exclude<PluginEntry, string>):
|
||||||
iframe.src = SANDBOX_DOC;
|
iframe.src = SANDBOX_DOC;
|
||||||
iframe.setAttribute('sandbox', 'allow-scripts'); // opaque origin — the isolation boundary
|
iframe.setAttribute('sandbox', 'allow-scripts'); // opaque origin — the isolation boundary
|
||||||
iframe.title = name;
|
iframe.title = name;
|
||||||
iframe.style.cssText = 'border:0;width:100%;height:0;display:block;background:transparent';
|
iframe.style.cssText = 'border:0;width:0;height:0;display:block;background:transparent;color-scheme:normal';
|
||||||
|
|
||||||
const chan = new MessageChannel();
|
|
||||||
const port = chan.port1;
|
|
||||||
const log = (...a: unknown[]) => { if (pluginDebug()) console.log(`%c[${name}]`, 'color:#8957e5', ...a); };
|
const log = (...a: unknown[]) => { if (pluginDebug()) console.log(`%c[${name}]`, 'color:#8957e5', ...a); };
|
||||||
|
|
||||||
let claimed = false;
|
let claimed = false;
|
||||||
|
|
@ -88,38 +86,50 @@ export async function mountSandboxedPlugin(entry: Exclude<PluginEntry, string>):
|
||||||
const slot = String(a[0] ?? 'footer_item') as UiSlot;
|
const slot = String(a[0] ?? 'footer_item') as UiSlot;
|
||||||
removeUi = usePluginRegistry.getState().addUi(slot, name, () => createElement(SandboxFrame, { iframe }));
|
removeUi = usePluginRegistry.getState().addUi(slot, name, () => createElement(SandboxFrame, { iframe }));
|
||||||
},
|
},
|
||||||
'ui.resize': (a) => { iframe.style.height = `${Math.max(0, Math.min(2000, Number(a[0]) || 0))}px`; },
|
'ui.resize': (a) => {
|
||||||
|
const w = Math.max(0, Math.min(4000, Number(a[0]) || 0));
|
||||||
|
const h = Math.max(0, Math.min(2000, Number(a[1]) || 0));
|
||||||
|
if (w) iframe.style.width = `${w}px`;
|
||||||
|
iframe.style.height = `${h}px`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
port.onmessage = (e: MessageEvent) => {
|
// (Re)establish the bridge on EVERY iframe load. Adopting the iframe into a UI
|
||||||
const m = e.data as GuestToHost;
|
// slot reparents it, which reloads the document and neuters the transferred
|
||||||
if (!m || m.type !== 'rpc' || typeof m.method !== 'string') return;
|
// port — so we hand the fresh guest a new MessageChannel and re-send init each
|
||||||
if (!isGranted(permissions, m.method)) {
|
// time (a plugin's on-load code must therefore be idempotent).
|
||||||
log('DENIED ungranted capability', m.method);
|
let teardown = () => {};
|
||||||
port.postMessage({ type: 'rpc:reply', id: m.id, error: `denied: ${m.method}` });
|
const handshake = () => {
|
||||||
return;
|
teardown();
|
||||||
}
|
const chan = new MessageChannel();
|
||||||
let result: unknown, error: string | undefined;
|
const port = chan.port1;
|
||||||
try { result = impl[m.method]?.(Array.isArray(m.args) ? m.args : []); }
|
port.onmessage = (e: MessageEvent) => {
|
||||||
catch (err) { error = String(err); }
|
const m = e.data as GuestToHost;
|
||||||
port.postMessage({ type: 'rpc:reply', id: m.id, result, error });
|
if (!m || m.type !== 'rpc' || typeof m.method !== 'string') return;
|
||||||
};
|
if (!isGranted(permissions, m.method)) {
|
||||||
|
log('DENIED ungranted capability', m.method);
|
||||||
// Forward only the curated events; refresh the cached snapshot when it can change.
|
port.postMessage({ type: 'rpc:reply', id: m.id, error: `denied: ${m.method}` });
|
||||||
const offs = FORWARDED_EVENTS.map((ev) =>
|
return;
|
||||||
bus.on(ev, (...args: unknown[]) => {
|
}
|
||||||
port.postMessage({ type: 'event', name: ev, args });
|
let result: unknown, error: string | undefined;
|
||||||
if (ev === 'connected' || ev === 'buffer.active') port.postMessage({ type: 'snapshot', snapshot: snapshot() });
|
try { result = impl[m.method]?.(Array.isArray(m.args) ? m.args : []); }
|
||||||
}));
|
catch (err) { error = String(err); }
|
||||||
void offs; // plugins live for the session; disposers kept for a future unload path
|
port.postMessage({ type: 'rpc:reply', id: m.id, result, error });
|
||||||
|
};
|
||||||
iframe.addEventListener('load', () => {
|
const offs = FORWARDED_EVENTS.map((ev) =>
|
||||||
|
bus.on(ev, (...args: unknown[]) => {
|
||||||
|
port.postMessage({ type: 'event', name: ev, args });
|
||||||
|
if (ev === 'connected' || ev === 'buffer.active') port.postMessage({ type: 'snapshot', snapshot: snapshot() });
|
||||||
|
}));
|
||||||
|
teardown = () => { offs.forEach((o) => o()); port.close(); };
|
||||||
iframe.contentWindow?.postMessage(
|
iframe.contentWindow?.postMessage(
|
||||||
{ type: 'init', name, permissions, source, snapshot: snapshot(), storage: loadStorage(name) },
|
{ type: 'init', name, permissions, source, snapshot: snapshot(), storage: loadStorage(name) },
|
||||||
'*', [chan.port2],
|
'*', [chan.port2],
|
||||||
);
|
);
|
||||||
log('mounted');
|
log('handshake');
|
||||||
});
|
};
|
||||||
|
|
||||||
|
iframe.addEventListener('load', handshake);
|
||||||
holder().appendChild(iframe);
|
holder().appendChild(iframe);
|
||||||
void removeUi; // referenced by the (future) unload path
|
void removeUi; // referenced by the (future) unload path
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue