I’m currently working on finishing up my September Twelve Websites project, which will be called Hyperfov Page Previews. In essence, it’s a way of generating dynamic Wikipedia-style page previews for any link on a website.
The problem at hand
Here’s the problem that’s the focus of this evening: positioning. I’d like these page preview popups to be insertable into pretty much any page, and to start I kept things simple and just created an invisible element on top of the link target element. This works for most static cases, but It’s giving me some trouble when links break between lines. Here’s an illustration of that:
What’s going on under the hood here is that the multi-line link is covered by an invisible element that naively uses the height of the link, assuming that it’s a block. But it’s not a block, and so this has the effect of covering other links on the same line, making them impossible to hover:
It also means that resizing the page breaks the links; their position isn’t recalculated:
The new approach
So let’s fix it. What I’d like to do is use the original link element for listening for hover events, rather than needing to position an invisible element above every link. This should mean that the positioning works regardless of the specifics of how the target element is rendered; as long as it receives onmouseover
events it should work.
This will mean I’ll need to move the positioning logic out of the popup component and into the setup function. When the setup function is called, the script pulls out all the a
tags, gets their positions, and inserts custom popup components at those positions. The new approach will be to attach event listeners to the a
tags, capture their position onmouseover
, calculate the popover position, and pass it into the component for rendering.
Reflection
Just finished it up. I was able to move all the visibility toggling logic out of the component and into the positioning function. I also moved the data fetching out of the component as well. This actually presented an interesting problem encoding attributes to pass through to HTML elements.
I wrote the preview component as a custom web component in svelte. The way svelte works, you can add any arbitrary attribute to your web component and svelte will parse it as a prop. So you might add an element in your page:
<link-preview title="hello" />
And svelte would add the LinkPreview
component with a prop title containing the string “hello”.
Now the tricky bit. Attributes on HTML elements, for the most part, must be strings. But they also have to encode as HTML, so trying to make a stringified JSON an attribute would fail because of all those extra quotes:
<link-preview data="{"title": "test"}" />
We can get around this problem by base64 encoding the string prior to passing it though, so any characters that could mess with the attribute, primarily "
characters (but potentially others too) are gone. The browser’s btoa
method does this very nicely:
<link-preview data="eyJ0aXRsZSI6ICJ0ZXN0In0=" />
However, it turns out there’s a problem with this method. btoa
is limited to a small set of single-byte characters, so any UTF8-encoded characters won’t work. Since many websites are encoded with UTF8 and some of the preview data I got back from the worker reflected that, it can fail to encode correctly when characters like ’
are included:
{"title": "This is something I’d like to send"}
Luckily, there’s an approach for encoding UTF8 strings to binary, then to base64, and back again. Using it, I was able to encode all the stringified JSON and pass it through to the web component as attributes. Interesting little lesson there.
In the end, I was able to fix the problem and make nice, responsive previews that attach perfectly to their origin elements, without any of the weirdness from before. Here’s the full commit.