// Thank you to https://github.com/daviddarnes/heading-anchors // Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/ let globalInstanceIndex = 0; class HeadingAnchors extends HTMLElement { static register(tagName) { if ("customElements" in window) { customElements.define(tagName || "heading-anchors", HeadingAnchors); } } static attributes = { exclude: "data-ha-exclude", prefix: "prefix", content: "content", } static classes = { anchor: "ha", placeholder: "ha-placeholder", srOnly: "ha-visualhide", } static defaultSelector = "h2,h3,h4,h5,h6"; static css = ` .${HeadingAnchors.classes.srOnly} { clip: rect(0 0 0 0); height: 1px; overflow: hidden; position: absolute; width: 1px; } .${HeadingAnchors.classes.anchor} { position: absolute; left: var(--ha_offsetx); top: var(--ha_offsety); text-decoration: none; opacity: 0; } .${HeadingAnchors.classes.placeholder} { opacity: .3; } .${HeadingAnchors.classes.anchor}:is(:focus-within, :hover) { opacity: 1; } .${HeadingAnchors.classes.anchor}, .${HeadingAnchors.classes.placeholder} { padding: 0 .25em; /* Disable selection of visually hidden label */ -webkit-user-select: none; user-select: none; } @supports (anchor-name: none) { .${HeadingAnchors.classes.anchor} { position: absolute; left: anchor(left); top: anchor(top); } }`; get supports() { return "replaceSync" in CSSStyleSheet.prototype; } get supportsAnchorPosition() { return CSS.supports("anchor-name: none"); } constructor() { super(); if(!this.supports) { return; } let sheet = new CSSStyleSheet(); sheet.replaceSync(HeadingAnchors.css); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; this.headingStyles = {}; this.instanceIndex = globalInstanceIndex++; } connectedCallback() { if (!this.supports) { return; } this.headings.forEach((heading, index) => { if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) { let anchor = this.getAnchorElement(heading); let placeholder = this.getPlaceholderElement(); // Prefers anchor position approach for better accessibility // https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/ if(this.supportsAnchorPosition) { let anchorName = `--ha_${this.instanceIndex}_${index}`; placeholder.style.setProperty("anchor-name", anchorName); anchor.style.positionAnchor = anchorName; } heading.appendChild(placeholder) heading.after(anchor); } }); } // Polyfill-only positionAnchorFromPlaceholder(placeholder) { if(!placeholder) { return; } let heading = placeholder.closest("h1,h2,h3,h4,h5,h6"); if(!heading.nextElementSibling) { return; } // TODO next element could be more defensive this.positionAnchor(heading.nextElementSibling); } // Polyfill-only positionAnchor(anchor) { if(!anchor || !anchor.previousElementSibling) { return; } // TODO previous element could be more defensive let heading = anchor.previousElementSibling; this.setFontProp(heading, anchor); if(this.supportsAnchorPosition) { // quit early return; } let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`); if(placeholder) { anchor.style.setProperty("--ha_offsetx", `${placeholder.offsetLeft}px`); anchor.style.setProperty("--ha_offsety", `${placeholder.offsetTop}px`); } } setFontProp(heading, anchor) { let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`); if(placeholder) { let style = getComputedStyle(placeholder); let props = ["font-weight", "font-size", "line-height", "font-family"]; let font = props.map(name => style.getPropertyValue(name)); anchor.style.setProperty("font", `${font[0]} ${font[1]}/${font[2]} ${font[3]}`); } } getAccessibleTextPrefix() { // Useful for i18n return this.getAttribute(HeadingAnchors.attributes.prefix) || "Jump to section titled"; } getContent() { return this.getAttribute(HeadingAnchors.attributes.content) || "#"; } getPlaceholderElement() { let ph = document.createElement("span"); ph.setAttribute("aria-hidden", true); ph.classList.add(HeadingAnchors.classes.placeholder); ph.textContent = this.getContent(); ph.addEventListener("mouseover", (e) => { let placeholder = e.target.closest(`.${HeadingAnchors.classes.placeholder}`); if(placeholder) { this.positionAnchorFromPlaceholder(placeholder); } }); return ph; } getAnchorElement(heading) { let anchor = document.createElement("a"); anchor.href = `#${heading.id}`; anchor.classList.add(HeadingAnchors.classes.anchor); let content = this.getContent(); anchor.innerHTML = `${this.getAccessibleTextPrefix()}: ${heading.textContent}`; anchor.addEventListener("focus", e => { let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`); if(anchor) { this.positionAnchor(anchor); } }); anchor.addEventListener("mouseover", (e) => { // when CSS anchor positioning is supported, this is only used to set the font let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`); this.positionAnchor(anchor); }); return anchor; } get headings() { return this.querySelectorAll(this.selector.split(",").map(entry => `${entry.trim()}[id]`)); } get selector() { return this.getAttribute("selector") || HeadingAnchors.defaultSelector; } } HeadingAnchors.register(); export { HeadingAnchors }