Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 126 additions & 2 deletions src/core/dfn-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ import { norm } from "./utils.js";

export const name = "core/dfn-panel";

const IDL_TYPES = new Set([
"attribute",
"callback",
"callback interface",
"constructor",
"dict-member",
"dictionary",
"enum-value",
"enum",
"exception",
"extended-attribute",
"interface",
"interface mixin",
"method",
"namespace",
"typedef",
]);

const CDDL_TYPES = new Set(["cddl-type", "cddl-key", "cddl-value"]);

const ELEMENT_TYPES = new Set(["element", "element-attr", "attr-value"]);

export async function run() {
document.head.insertBefore(
html`<style>
Expand Down Expand Up @@ -72,6 +94,7 @@ function createPanel(dfn) {
${dfnExportedMarker(dfn)} ${idlMarker(dfn, links)}
${cddlMarker(dfn, links)}
</div>
${linkingSyntaxesToHTML(dfn)}
<p><b>Referenced in:</b></p>
${referencesToHTML(id, links)}
</div>
Expand Down Expand Up @@ -134,14 +157,115 @@ function cddlMarker(dfn, links) {
>`;
}

/**
* Returns the linking syntax string for a term given its dfn-type and dfn-for.
* @param {string} term
* @param {string} dfnType
* @param {string | null} dfnFor
* @returns {string}
*/
function termToSyntax(term, dfnType, dfnFor) {
const forPrefix = dfnFor ? `${dfnFor}/` : "";
if (IDL_TYPES.has(dfnType)) {
return `{{${forPrefix}${term}}}`;
}
if (CDDL_TYPES.has(dfnType)) {
return `{^${forPrefix}${term}^}`;
}
if (ELEMENT_TYPES.has(dfnType)) {
if ((dfnType === "element-attr" || dfnType === "attr-value") && dfnFor) {
return `[^${dfnFor}/${term}^]`;
}
return `[^${term}^]`;
}
if (dfnFor) {
return `[=${dfnFor}/${term}=]`;
}
return `[=${term}=]`;
}

/**
* Returns the list of possible linking syntax strings for a dfn element.
* Includes the primary text and any data-lt aliases.
* @param {HTMLElement} dfn
* @returns {string[]}
*/
function getLinkingSyntaxes(dfn) {
const dfnType = dfn.dataset.dfnType || "dfn";
const forValues = dfn.dataset.dfnFor
? dfn.dataset.dfnFor
.split(",")
.map(s => s.trim())
.filter(Boolean)
: [null];
const primaryTerm = "ltNodefault" in dfn.dataset ? "" : norm(dfn.textContent);
const ltTerms = dfn.dataset.lt
? dfn.dataset.lt.split("|").map(norm).filter(Boolean)
: [];
const allTerms = [primaryTerm, ...ltTerms].filter(
(t, i, arr) => Boolean(t) && arr.indexOf(t) === i
);
return allTerms.flatMap(term =>
forValues.map(forValue => termToSyntax(term, dfnType, forValue))
);
}

/**
* Creates a copy-to-clipboard button for a linking syntax string.
* @param {string} text
* @returns {HTMLButtonElement}
*/
function createSyntaxCopyButton(text) {
const button = document.createElement("button");
button.className = "dfn-panel-copy-btn removeOnSave";
button.dataset.copyText = text;
button.setAttribute("aria-label", `Copy ${text} to clipboard`);
button.title = "Copy to clipboard";
button.textContent = "⎘";
return button;
}

/**
* Renders the "Possible linking syntaxes:" section for a dfn panel.
* Returns null if the dfn is an index-term (external) or has no syntaxes.
* @param {HTMLElement} dfn
* @returns {HTMLElement | null}
*/
function linkingSyntaxesToHTML(dfn) {
// Only show for local <dfn> elements, not external .index-term spans
if (!dfn.matches("dfn")) return null;
const syntaxes = getLinkingSyntaxes(dfn);
if (!syntaxes.length) return null;

const ul = document.createElement("ul");
ul.className = "dfn-panel-lt";
for (const syntax of syntaxes) {
const li = document.createElement("li");
const code = document.createElement("code");
code.textContent = syntax;
const copyBtn = createSyntaxCopyButton(syntax);
li.append(code, " ", copyBtn);
ul.append(li);
}

const b = document.createElement("b");
b.textContent = "Possible linking syntaxes:";
const p = document.createElement("p");
p.append(b);

const container = document.createElement("div");
container.append(p, ul);
return container;
}

/**
* @param {string} id dfn id
* @param {NodeListOf<HTMLAnchorElement>} links
* @returns {HTMLUListElement}
*/
function referencesToHTML(id, links) {
if (!links.length) {
return html`<ul>
return html`<ul class="dfn-panel-refs">
<li>Not referenced in this document.</li>
</ul>`;
}
Expand Down Expand Up @@ -187,7 +311,7 @@ function referencesToHTML(id, links) {
</li>`;
};

return html`<ul>
return html`<ul class="dfn-panel-refs">
${[...titleToIDs].map(listItemToHTML)}
</ul>`;
}
Expand Down
44 changes: 34 additions & 10 deletions src/core/dfn-panel.runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ function setupPanel() {
const listener = panelListener();
document.body.addEventListener("keydown", listener);
document.body.addEventListener("click", listener);
setupCopyButtons();
}

function setupCopyButtons() {
document.body.addEventListener("click", event => {
const target = event.target;
const btn =
target instanceof HTMLElement && target.closest(".dfn-panel-copy-btn");
if (!(btn instanceof HTMLElement)) return;
const text = btn.dataset.copyText;
if (!text) return;
navigator.clipboard.writeText(text).catch(() => {});
});
}

function panelListener() {
Expand Down Expand Up @@ -99,6 +112,10 @@ function deriveAction(event) {
return hitALink ? "none" : "show";
}
if (target.closest(".dfn-panel")) {
// Copy buttons keep the panel open regardless of docked state.
if (target.closest(".dfn-panel-copy-btn")) {
return "none";
}
if (hitALink) {
return target.classList.contains("self-link") ? "hide" : "dock";
}
Expand Down Expand Up @@ -164,16 +181,18 @@ function displayPanel(dfn, panel, { x, y }) {
* @returns
*/
function trapFocus(panel, dfn) {
/** @type NodeListOf<HTMLAnchorElement> elements */
const anchors = panel.querySelectorAll("a[href]");
/** @type NodeListOf<HTMLElement> elements */
const focusableElements = panel.querySelectorAll(
"a[href], button:not([disabled])"
);
// No need to trap focus
if (!anchors.length) return;
if (!focusableElements.length) return;

// Move focus to first anchor element
const first = anchors.item(0);
const first = focusableElements.item(0);
first.focus();

const trapListener = createTrapListener(anchors, panel, dfn);
const trapListener = createTrapListener(focusableElements, panel, dfn);
panel.addEventListener("keydown", trapListener);

// Hiding the panel releases the trap
Expand All @@ -190,13 +209,13 @@ function trapFocus(panel, dfn) {

/**
*
* @param {NodeListOf<HTMLAnchorElement>} anchors
* @param {NodeListOf<HTMLElement>} focusableElements
* @param {HTMLElement} panel
* @param {HTMLElement} dfn
* @returns
*/
function createTrapListener(anchors, panel, dfn) {
const lastIndex = anchors.length - 1;
function createTrapListener(focusableElements, panel, dfn) {
const lastIndex = focusableElements.length - 1;
let currentIndex = 0;
/**
* @param {KeyboardEvent} event
Expand All @@ -212,13 +231,18 @@ function createTrapListener(anchors, panel, dfn) {
} else if (currentIndex > lastIndex) {
currentIndex = 0;
}
anchors.item(currentIndex).focus();
focusableElements.item(currentIndex).focus();
break;
}

// Hitting "Enter" on an anchor releases the trap.
case "Enter":
hidePanel(panel);
if (
event.target instanceof HTMLElement &&
event.target.closest("a[href]")
) {
hidePanel(panel);
}
break;

// Hitting "Escape" returns focus to dfn.
Expand Down
29 changes: 29 additions & 0 deletions src/styles/dfn-panel.css.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,33 @@ dfn {
max-height: 30vh;
overflow: auto;
}

.dfn-panel .dfn-panel-lt {
list-style: none;
padding: 0;
}

.dfn-panel .dfn-panel-lt li {
display: flex;
align-items: center;
gap: 0.25em;
margin-left: 0;
}

.dfn-panel .dfn-panel-copy-btn {
display: inline-flex;
align-items: center;
padding: 0 0.2em;
border: 1px solid #ccc;
border-radius: 0.2em;
background: transparent;
font-size: 0.85em;
cursor: pointer;
line-height: 1;
color: inherit;
}

.dfn-panel .dfn-panel-copy-btn:hover {
background: #eee;
}
`;
4 changes: 2 additions & 2 deletions tests/spec/core/dfn-index-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,8 @@ describe("Core — dfn-index", () => {
expect(panel.querySelector("a.self-link").href).toBe(
"https://dom.spec.whatwg.org/#event"
);
expect(panel.querySelectorAll("ul li")).toHaveSize(1);
const reference = panel.querySelector("ul li a");
expect(panel.querySelectorAll(".dfn-panel-refs li")).toHaveSize(1);
const reference = panel.querySelector(".dfn-panel-refs li a");
expect(reference.textContent).toBe("§ 1. TEST");
expect(reference.hash).toBe("#ref-for-index-term-event-interface-1");
});
Expand Down
Loading