2024-07-16 16:45:08 -04:00
|
|
|
// Thank you to https://github.com/daviddarnes/heading-anchors
|
2024-08-26 15:53:52 -04:00
|
|
|
// Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
|
2024-07-16 16:45:08 -04:00
|
|
|
|
|
|
|
class HeadingAnchors extends HTMLElement {
|
|
|
|
static register(tagName) {
|
|
|
|
if ("customElements" in window) {
|
|
|
|
customElements.define(tagName || "heading-anchors", HeadingAnchors);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-26 15:53:52 -04:00
|
|
|
static attributes = {
|
2024-08-26 18:33:39 -04:00
|
|
|
exclude: "data-ha-exclude",
|
|
|
|
content: "data-ha-text"
|
2024-08-26 17:44:53 -04:00
|
|
|
}
|
2024-08-26 15:53:52 -04:00
|
|
|
|
|
|
|
static classes = {
|
2024-08-26 18:33:39 -04:00
|
|
|
anchor: "ha",
|
|
|
|
heading: "ha-h", // only used for nested method
|
2024-08-26 15:53:52 -04:00
|
|
|
}
|
|
|
|
|
2024-08-26 17:56:02 -04:00
|
|
|
static defaultSelector = "h2,h3,h4,h5,h6";
|
2024-08-09 16:48:14 -04:00
|
|
|
|
|
|
|
static featureTest() {
|
2024-08-26 15:53:52 -04:00
|
|
|
return ;
|
2024-08-09 16:48:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static css = `
|
2024-08-26 15:53:52 -04:00
|
|
|
.${HeadingAnchors.classes.anchor} {
|
2024-08-26 17:44:53 -04:00
|
|
|
position: absolute;
|
2024-08-09 16:48:14 -04:00
|
|
|
text-decoration: none;
|
2024-08-26 15:53:52 -04:00
|
|
|
font-weight: 400;
|
|
|
|
opacity: 0;
|
|
|
|
transition: opacity .15s;
|
|
|
|
padding-left: .25em;
|
|
|
|
padding-right: .25em;
|
2024-08-09 16:48:14 -04:00
|
|
|
}
|
2024-08-26 17:56:02 -04:00
|
|
|
|
2024-08-26 15:53:52 -04:00
|
|
|
/* nested */
|
2024-08-26 16:06:38 -04:00
|
|
|
:is(h1,h2,h3,h4,h5,h6):is(:focus-within, :hover) .${HeadingAnchors.classes.anchor},
|
2024-08-26 15:53:52 -04:00
|
|
|
/* sibling */
|
2024-08-26 16:06:38 -04:00
|
|
|
:is(h1,h2,h3,h4,h5,h6) + .${HeadingAnchors.classes.anchor}:is(:focus-within, :hover),
|
|
|
|
:is(h1,h2,h3,h4,h5,h6):is(:focus-within, :hover) + .${HeadingAnchors.classes.anchor} {
|
2024-08-09 17:38:07 -04:00
|
|
|
opacity: 1;
|
2024-08-09 16:48:14 -04:00
|
|
|
}
|
2024-08-26 15:53:52 -04:00
|
|
|
@supports (anchor-name: none) {
|
2024-08-26 17:56:02 -04:00
|
|
|
/* purely for anchoring */
|
|
|
|
.${HeadingAnchors.classes.heading}:after {
|
|
|
|
content: "";
|
|
|
|
anchor-name: var(--ha_anchor);
|
|
|
|
}
|
2024-08-26 15:53:52 -04:00
|
|
|
.${HeadingAnchors.classes.anchor} {
|
2024-08-26 17:56:02 -04:00
|
|
|
left: anchor(right);
|
2024-08-26 15:53:52 -04:00
|
|
|
top: anchor(top);
|
|
|
|
}
|
2024-08-09 16:48:14 -04:00
|
|
|
}`;
|
|
|
|
|
2024-08-26 15:53:52 -04:00
|
|
|
get supportsTest() {
|
|
|
|
return "replaceSync" in CSSStyleSheet.prototype;
|
|
|
|
}
|
|
|
|
|
|
|
|
get supportsAnchorPosition() {
|
|
|
|
return CSS.supports("anchor-name: none");
|
|
|
|
}
|
|
|
|
|
2024-08-09 16:48:14 -04:00
|
|
|
constructor() {
|
2024-08-26 15:53:52 -04:00
|
|
|
super();
|
|
|
|
|
|
|
|
if(!this.supportsTest) {
|
2024-08-09 16:48:14 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let sheet = new CSSStyleSheet();
|
|
|
|
sheet.replaceSync(HeadingAnchors.css);
|
|
|
|
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
|
|
}
|
|
|
|
|
2024-07-16 16:45:08 -04:00
|
|
|
connectedCallback() {
|
2024-08-26 15:53:52 -04:00
|
|
|
if (!this.supportsTest) {
|
2024-08-09 16:48:14 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-26 15:53:52 -04:00
|
|
|
this.headings.forEach((heading, index) => {
|
|
|
|
if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) {
|
2024-08-09 16:48:14 -04:00
|
|
|
let anchor = this.getAnchorElement(heading);
|
2024-08-26 15:53:52 -04:00
|
|
|
|
|
|
|
// Prefers anchor position approach for better accessibility
|
|
|
|
// https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
|
|
|
|
if(this.supportsAnchorPosition) {
|
2024-08-26 17:44:53 -04:00
|
|
|
let anchorName = `--ha_${index}`;
|
2024-08-26 17:56:02 -04:00
|
|
|
heading.style.setProperty("--ha_anchor", anchorName);
|
2024-08-26 15:53:52 -04:00
|
|
|
anchor.style.positionAnchor = anchorName;
|
|
|
|
|
|
|
|
let fontSize = parseInt(getComputedStyle(heading).getPropertyValue("font-size"), 10);
|
2024-08-26 17:44:53 -04:00
|
|
|
anchor.style.fontSize = `${fontSize / 16}em`;
|
2024-08-26 15:53:52 -04:00
|
|
|
|
2024-08-26 17:56:02 -04:00
|
|
|
heading.classList.add(HeadingAnchors.classes.heading);
|
2024-08-26 15:53:52 -04:00
|
|
|
heading.after(anchor);
|
2024-08-09 16:48:14 -04:00
|
|
|
} else {
|
|
|
|
heading.appendChild(anchor);
|
|
|
|
}
|
2024-07-16 16:45:08 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-26 18:33:39 -04:00
|
|
|
getText(heading) {
|
|
|
|
return heading.getAttribute(HeadingAnchors.attributes.content) || "#";
|
|
|
|
}
|
|
|
|
|
2024-08-09 16:48:14 -04:00
|
|
|
getAnchorElement(heading) {
|
2024-07-16 16:45:08 -04:00
|
|
|
let anchor = document.createElement("a");
|
|
|
|
anchor.href = `#${heading.id}`;
|
2024-08-26 15:53:52 -04:00
|
|
|
anchor.classList.add(HeadingAnchors.classes.anchor);
|
2024-08-26 18:33:39 -04:00
|
|
|
|
|
|
|
let text = this.getText(heading);
|
2024-08-26 15:53:52 -04:00
|
|
|
if(this.supportsAnchorPosition) {
|
2024-08-26 18:33:39 -04:00
|
|
|
anchor.innerHTML = `<span class="visually-hidden">Jump to section titled: ${heading.textContent}</span><span aria-hidden="true">${text}</span>`;
|
2024-08-26 15:53:52 -04:00
|
|
|
} else {
|
2024-08-26 18:33:39 -04:00
|
|
|
anchor.innerHTML = `<span aria-hidden="true">${text}</span>`;
|
2024-08-26 15:53:52 -04:00
|
|
|
}
|
|
|
|
|
2024-07-16 16:45:08 -04:00
|
|
|
return anchor;
|
|
|
|
}
|
|
|
|
|
|
|
|
get headings() {
|
|
|
|
return this.querySelectorAll(this.selector.split(",").map(entry => `${entry.trim()}[id]`));
|
|
|
|
}
|
|
|
|
|
|
|
|
get selector() {
|
2024-08-09 16:48:14 -04:00
|
|
|
return this.getAttribute("selector") || HeadingAnchors.defaultSelector;
|
2024-07-16 16:45:08 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
HeadingAnchors.register();
|