Skip to content

Commit

Permalink
Add reference hint popup on hover of links (#2708)
Browse files Browse the repository at this point in the history
* Initial working version of ref hints

* cleanup

* Only add ref for each url to refsData one time.

* Simplify

* Add visual test.

* Add DFN text if different from link text

* Fix missing links

* Add auto-hide after one second delay

* Don't clear timeout if not set.

* Handle focus and blur events.

* Fix teardown

* --rebase

* Fix formatting to make black happy.

---------

Co-authored-by: Tab Atkins-Bittner <[email protected]>
  • Loading branch information
dlaliberte and tabatkins authored Nov 10, 2023
1 parent 7884fd2 commit dba9c3d
Show file tree
Hide file tree
Showing 326 changed files with 109,283 additions and 1,030 deletions.
3 changes: 0 additions & 3 deletions bikeshed/dfnpanels/dfnpanels.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use strict";
{
const dfnsJson = window.dfnsJson || {};

// Functions, divided by link type, that wrap an autolink's
// contents with the appropriate outer syntax.
// Alternately, a string naming another type they format
Expand Down Expand Up @@ -249,7 +247,6 @@
for (const el of tabbable) {
el.tabIndex = tabIndex++;
}

}

function positionDfnPanel(dfnPanel) {
Expand Down
35 changes: 35 additions & 0 deletions bikeshed/refs/refhints.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
:root {
--ref-hint-bg: #ddd;
--ref-hint-text: var(--text);
}
@media (prefers-color-scheme: dark) {
:root {
--ref-hint-bg: #222;
--ref-hint-text: var(--text);
}
}

.ref-hint {
display: none;
position: absolute;
z-index: 35;
width: 20em;
width: 300px;
height: auto;
max-height: 500px;
overflow: auto;
padding: 0.5em 0.5em;
font: small Helvetica Neue, sans-serif, Droid Sans Fallback;
background: var(--ref-hint-bg);
color: var(--ref-hint-text);
border: outset 0.2em;
white-space: normal; /* in case it's moved into a pre */
}

.ref-hint.on {
display: inline-block;
}

.ref-hint * { margin: 0; padding: 0; text-indent: 0; }

.ref-hint ul { padding: 0 0 0 1em; list-style: none; }
207 changes: 207 additions & 0 deletions bikeshed/refs/refhints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"use strict";
{
function genRefHint(link, ref) {
const linkText = link.textContent;
let dfnTextElements = '';
if (ref.text != linkText) {
dfnTextElements =
mk.li({},
mk.b({}, "DFN: "),
mk.code({ class: "dfn-text" }, ref.text)
);
}
const forList = ref.for_;
const forListElements = forList.length === 0 ? '' : mk.li({},
mk.b({}, "For: "),
mk.ul({},
...forList.map(forItem =>
mk.li({},
mk.span({}, `${forItem}`)
),
),
),
);
const url = ref.url;
const safeUrl = encodeURIComponent(url);
const hintPanel = mk.aside({
class: "ref-hint",
id: `ref-hint-for-${safeUrl}`,
"data-for": url,
"aria-labelled-by": `ref-hint-for-${safeUrl}`,
},
mk.ul({},
dfnTextElements,
mk.li({},
mk.b({}, "URL: "),
mk.a({ href: url, class: "ref" }, url),
),
mk.li({},
mk.b({}, "Type: "),
mk.span({}, `${ref.type}`),
),
mk.li({},
mk.b({}, "Spec: "),
mk.span({}, `${ref.spec ? ref.spec : ''}`),
),
forListElements
),
);
hintPanel.forLink = link;
return hintPanel;
}
function genAllRefHints() {
for(const refData of Object.values(window.refsData)) {
const refUrl = refData.url;
const links = document.querySelectorAll(`a[href="${refUrl}"]`);
if (links.length == 0) {
console.log(`Can't find link href="${refUrl}".`, refData);
continue;
}
for (let i = 0; i < links.length; i++) {
const link = links[i];
const hint = genRefHint(link, refData);
append(document.body, hint);
insertLinkPopupAction(hint)
}
}
}

function hideAllRefHints() {
queryAll(".ref-hint.on").forEach(el=>hideRefHint(el));
}

function hideRefHint(refHint) {
const link = refHint.forLink;
link.setAttribute("aria-expanded", "false");
refHint.style.position = "absolute"; // unfix it
refHint.classList.remove("on");
const teardown = refHint.teardownEventListeners;
if (teardown) {
refHint.teardownEventListeners = undefined;
teardown();
}
}

function showRefHint(refHint) {
hideAllRefHints(); // Only display one at this time.

const link = refHint.forLink;
link.setAttribute("aria-expanded", "true");
refHint.classList.add("on");
positionRefHint(refHint);
setupRefHintEventListeners(refHint);
}

function setupRefHintEventListeners(refHint) {
if (refHint.teardownEventListeners) return;
const link = refHint.forLink;
// Add event handlers to hide the refHint after the user moves away
// from both the link and refHint, if not hovering either within one second.
let timeout = null;
const startHidingRefHint = (event) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
hideRefHint(refHint);
}, 1000);
}
const resetHidingRefHint = (event) => {
if (timeout) clearTimeout(timeout);
timeout = null;
};
link.addEventListener("mouseleave", startHidingRefHint);
link.addEventListener("mouseenter", resetHidingRefHint);
link.addEventListener("blur", startHidingRefHint);
link.addEventListener("focus", resetHidingRefHint);
refHint.addEventListener("mouseleave", startHidingRefHint);
refHint.addEventListener("mouseenter", resetHidingRefHint);
refHint.addEventListener("blur", startHidingRefHint);
refHint.addEventListener("focus", resetHidingRefHint);

refHint.teardownEventListeners = () => {
// remove event listeners
resetHidingRefHint();
link.removeEventListener("mouseleave", startHidingRefHint);
link.removeEventListener("mouseenter", resetHidingRefHint);
link.removeEventListener("blur", startHidingRefHint);
link.removeEventListener("focus", resetHidingRefHint);
refHint.removeEventListener("mouseleave", startHidingRefHint);
refHint.removeEventListener("mouseenter", resetHidingRefHint);
refHint.removeEventListener("blur", startHidingRefHint);
refHint.removeEventListener("focus", resetHidingRefHint);
};
}

function positionRefHint(refHint) {
const link = refHint.forLink;
const linkPos = getRootLevelAbsolutePosition(link);
refHint.style.top = linkPos.bottom + "px";
refHint.style.left = linkPos.left + "px";

const panelPos = refHint.getBoundingClientRect();
const panelMargin = 8;
const maxRight = document.body.parentNode.clientWidth - panelMargin;
if (panelPos.right > maxRight) {
const overflowAmount = panelPos.right - maxRight;
const newLeft = Math.max(panelMargin, linkPos.left - overflowAmount);
refHint.style.left = newLeft + "px";
}
}

// TODO: shared util
// Returns the root-level absolute position {left and top} of element.
function getRootLevelAbsolutePosition(el) {
const boundsRect = el.getBoundingClientRect();
let xPos = 0;
let yPos = 0;

while (el) {
let xScroll = el.scrollLeft;
let yScroll = el.scrollTop;

// Ignore scrolling of body.
if (el.tagName === "BODY") {
xScroll = 0;
yScroll = 0;
}
xPos += (el.offsetLeft - xScroll + el.clientLeft);
yPos += (el.offsetTop - yScroll + el.clientTop);

el = el.offsetParent;
}
return {
left: xPos,
top: yPos,
right: xPos + boundsRect.width,
bottom: yPos + boundsRect.height,
};
}

function insertLinkPopupAction(refHint) {
const link = refHint.forLink;
// link.setAttribute('role', 'button');
link.setAttribute('aria-expanded', 'false')
link.classList.add('has-ref-hint');
link.addEventListener('mouseover', (event) => {
showRefHint(refHint);
});
link.addEventListener('focus', (event) => {
showRefHint(refHint);
})
}

document.addEventListener("DOMContentLoaded", () => {
genAllRefHints();

document.body.addEventListener("click", (e) => {
// If not handled already, just hide all link panels.
hideAllRefHints();
});
});

window.addEventListener("resize", () => {
// Hide any open ref hint.
hideAllRefHints();
});
}
17 changes: 17 additions & 0 deletions bikeshed/unsortedJunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,9 @@ def verifyUsageOfAllLocalBiblios(doc: t.SpecT) -> None:


def processAutolinks(doc: t.SpecT) -> None:
scriptLines = []
refsAdded = {}

# An <a> without an href is an autolink.
# <i> is a legacy syntax for term autolinks. If it links up, we change it into an <a>.
# We exclude bibliographical links, as those are processed in `processBiblioLinks`.
Expand Down Expand Up @@ -762,13 +765,27 @@ def processAutolinks(doc: t.SpecT) -> None:
h.clearContents(el)
el.text = replacementText
decorateAutolink(doc, el, linkType=linkType, linkText=linkText, ref=ref)

if ref.url not in refsAdded:
refsAdded[ref.url] = True
refJson = ref.__json__()
scriptLines.append(f"window.refsData['{ref.url}'] = {json.dumps(refJson)};")
else:
if linkType == "maybe":
el.tag = "css"
if el.get("data-link-type"):
del el.attrib["data-link-type"]
if el.get("data-lt"):
del el.attrib["data-lt"]

if len(scriptLines) > 0:
jsonBlock = doc.extraScripts.setDefault("ref-hints-json", "window.refsData = {};\n")
jsonBlock.text += "\n".join(scriptLines)

doc.extraScripts.setFile("ref-hints", "refs/refhints.js")
doc.extraStyles.setFile("ref-hints", "refs/refhints.css")
h.addDOMHelperScript(doc)

h.dedupIDs(doc)


Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit dba9c3d

Please sign in to comment.