Hi @shadoof, there’s no easy solution to this right now, but let me offer some background and two workarounds (each with its own caveats). As for the feature request, I’d recommend to file that in Observable’s feedback repository as it might get lost otherwise.
Background
When a notebook gets embedded as an iframe there are three browsing contexts in which users can navigate:
_top
, the embedding page. Normally this is where any navigation should happen to avoid opening pages within the iframe.
_parent
: The iframe URL that displays the attribution frame and the inner sandbox iframe. This is where any cell IDs reside, and where in-page navigation for anchors would need to happen.
_self
: The sandbox itself. Navigating in this context would immediately break the notebook (you may have seen this on occasion when clicking a link in an SVG).
Observable sets the sandbox iframe’s document.baseURI
(the <base>
element) to open links in the _top
context, using the notebook’s non-embed URL. This ensures that any relative links open correctly. This base URI is also the reason that you see a link #namedCell
being opened in the top-level browsing context (instead of just scrolling to the offset).
Workaround 1: Scroll in _parent
We can only scroll the parent iframe without breaking the notebook if we precisely match the URL that the iframe was embedded with. However, we cannot simply read the parent iframe’s URL (because we’re in a sandbox!), and Observable strips any cell
and cells
parameters. We’ll need to pass them in separately in order to reconstruct the parent URL.
Let’s create a helper that allows us to use local links. In order for the helper to work the following conditions need to be met:
- the iframe URL also contains the query parameter
_cell
or _cells
(after the unaliased parameter), and the aliased parameter matches the original parameter exactly
- the iframe URL is unpinned (i.e., has no version specifier)
Here’s the helper code:
local = {
const url = new URL(document.baseURI);
let target = null;
let base = url;
const cell = url.searchParams.get("_cell");
const cells = url.searchParams.get("_cells");
if(cell || cells) {
url.search = new URLSearchParams([
...Object.entries({cell, cells}).filter(d => d[1]),
...url.searchParams.entries()
]);
url.pathname = url.pathname.replace(/^\/(d\/|)/, "/embed/");
target = "_parent";
}
return (text, href) => htl.html`<a ${{target, href: new URL(href, base)}}>${text}`;
}
It can be used as follows:
For details see ${local("this named cell", "#namedCell")}
This may very well be the most fragile solution, so I’d only recommend it as a last resort.
Workaround 2: Scroll in _self
As mentioned earlier, the IDs for named cells reside in the parent iframe, which means that we can’t access the corresponding elements directly.
We can however insert our own anchors, for example:
<span id=namedCell />Some more text after this invisible target element
We can still not link directly to it, which is where the second part of the solution comes in:
handleLocal = {
const onClick = e => {
if(!e.target.matches("a[href^='#']")) return;
const target = document.querySelector(e.target.getAttribute("href"));
if(target) e.preventDefault(), target.scrollIntoView();
};
document.addEventListener("click", onClick);
invalidation.then(() => document.removeEventListener("click", onClick));
// Don't display anything.
return htl.html`<div>`;
}
This snippet captures any clicks on local links and scrolls the target element into view (if it was found).
We can also combine these solutions to handle IDs both in _self
and _parent
. Assuming that our notebook already contains the local
helper that I shared above, we can modify handleLocal
as follows:
handleLocal = {
const onClick = e => {
if(!e.target.matches("a[href^='#']")) return;
e.preventDefault();
const href = e.target.getAttribute("href");
const target = document.querySelector(href);
target ? target.scrollIntoView() : local("", href).click();
};
document.addEventListener("click", onClick);
invalidation.then(() => document.removeEventListener("click", onClick));
// Don't display anything.
return htl.html`<div>`;
}
What this does:
- check if the clicked link is a local link
- if the link target exists in the document, scroll it into view
- otherwise attempt to navigate to it in the _parent context