1// Thank you to https://github.com/daviddarnes/heading-anchors 2// Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/ 3 4let globalInstanceIndex = 0; 5 6class HeadingAnchors extends HTMLElement { 7 static register(tagName = "heading-anchors", registry = window.customElements) { 8 if(registry && !registry.get(tagName)) { 9 registry.define(tagName, this); 10 } 11 } 12 13 static attributes = { 14 exclude: "data-ha-exclude", 15 prefix: "prefix", 16 content: "content", 17 } 18 19 static classes = { 20 anchor: "ha", 21 placeholder: "ha-placeholder", 22 srOnly: "ha-visualhide", 23 } 24 25 static defaultSelector = "h2,h3,h4,h5,h6"; 26 27 static css = ` 28.${HeadingAnchors.classes.srOnly} { 29 clip: rect(0 0 0 0); 30 height: 1px; 31 overflow: hidden; 32 position: absolute; 33 width: 1px; 34} 35.${HeadingAnchors.classes.anchor} { 36 position: absolute; 37 left: var(--ha_offsetx); 38 top: var(--ha_offsety); 39 text-decoration: none; 40 opacity: 0; 41} 42.${HeadingAnchors.classes.placeholder} { 43 opacity: .3; 44} 45.${HeadingAnchors.classes.anchor}:is(:focus-within, :hover) { 46 opacity: 1; 47} 48.${HeadingAnchors.classes.anchor}, 49.${HeadingAnchors.classes.placeholder} { 50 display: inline-block; 51 padding: 0 .25em; 52 53 /* Disable selection of visually hidden label */ 54 -webkit-user-select: none; 55 user-select: none; 56} 57 58@supports (anchor-name: none) { 59 .${HeadingAnchors.classes.anchor} { 60 position: absolute; 61 left: anchor(left); 62 top: anchor(top); 63 } 64}`; 65 66 get supports() { 67 return "replaceSync" in CSSStyleSheet.prototype; 68 } 69 70 get supportsAnchorPosition() { 71 return CSS.supports("anchor-name: none"); 72 } 73 74 constructor() { 75 super(); 76 77 if(!this.supports) { 78 return; 79 } 80 81 let sheet = new CSSStyleSheet(); 82 sheet.replaceSync(HeadingAnchors.css); 83 document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; 84 85 this.headingStyles = {}; 86 this.instanceIndex = globalInstanceIndex++; 87 } 88 89 connectedCallback() { 90 if (!this.supports) { 91 return; 92 } 93
94 this.headings.forEach((heading, index) => { 95 if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) { 96 let anchor = this.getAnchorElement(heading); 97 let placeholder = this.getPlaceholderElement(); 98 99 // Prefers anchor position approach for better accessibility 100 // https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/ 101 if(this.supportsAnchorPosition) { 102 let anchorName = `--ha_${this.instanceIndex}_${index}`; 103 placeholder.style.setProperty("anchor-name", anchorName); 104 anchor.style.positionAnchor = anchorName; 105 } 106 107 heading.appendChild(placeholder); 108 heading.after(anchor); 109 } 110 }); 111 } 112 113 // Polyfill-only 114 positionAnchorFromPlaceholder(placeholder) { 115 if(!placeholder) { 116 return; 117 } 118 119 let heading = placeholder.closest("h1,h2,h3,h4,h5,h6"); 120 if(!heading.nextElementSibling) { 121 return; 122 } 123 124 // TODO next element could be more defensive 125 this.positionAnchor(heading.nextElementSibling); 126 } 127 128 // Polyfill-only 129 positionAnchor(anchor) { 130 if(!anchor || !anchor.previousElementSibling) { 131 return; 132 } 133 134 // TODO previous element could be more defensive 135 let heading = anchor.previousElementSibling; 136 this.setFontProp(heading, anchor); 137 138 if(this.supportsAnchorPosition) { 139 // quit early 140 return; 141 } 142 143 let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`); 144 if(placeholder) { 145 anchor.style.setProperty("--ha_offsetx", `${placeholder.offsetLeft}px`); 146 anchor.style.setProperty("--ha_offsety", `${placeholder.offsetTop}px`); 147 } 148 } 149 150 setFontProp(heading, anchor) { 151 let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`); 152 if(placeholder) { 153 let style = getComputedStyle(placeholder); 154 let props = ["font-weight", "font-size", "line-height", "font-family"]; 155 let [weight, size, lh, family] = props.map(name => style.getPropertyValue(name)); 156 anchor.style.setProperty("font", `${weight} ${size}/${lh} ${family}`); 157 let vars = style.getPropertyValue("font-variation-settings"); 158 if(vars) { 159 anchor.style.setProperty("font-variation-settings", vars); 160 } 161 } 162 } 163 164 getAccessibleTextPrefix() { 165 // Useful for i18n 166 return this.getAttribute(HeadingAnchors.attributes.prefix) || "Jump to section titled"; 167 } 168 169 getContent() { 170 if(this.hasAttribute(HeadingAnchors.attributes.content)) { 171 return this.getAttribute(HeadingAnchors.attributes.content); 172 } 173 return "#"; 174 } 175 176 // Placeholder nests inside of heading 177 getPlaceholderElement() { 178 let ph = document.createElement("span"); 179 ph.setAttribute("aria-hidden", true); 180 ph.classList.add(HeadingAnchors.classes.placeholder); 181 let content = this.getContent(); 182 if(content) { 183 ph.textContent = content; 184 } 185 186 ph.addEventListener("mouseover", (e) => { 187 let placeholder = e.target.closest(`.${HeadingAnchors.classes.placeholder}`); 188 if(placeholder) { 189 this.positionAnchorFromPlaceholder(placeholder); 190 } 191 }); 192 193 return ph; 194 } 195 196 getAnchorElement(heading) { 197 let anchor = document.createElement("a"); 198 anchor.href = `#${heading.id}`; 199 anchor.classList.add(HeadingAnchors.classes.anchor); 200 201 let content = this.getContent(); 202 anchor.innerHTML = `<span class="${HeadingAnchors.classes.srOnly}">${this.getAccessibleTextPrefix()}: ${heading.textContent}</span>${content ? `<span aria-hidden="true">${content}</span>` : ""}`; 203 204 anchor.addEventListener("focus", e => { 205 let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`); 206 if(anchor) { 207 this.positionAnchor(anchor); 208 } 209 }); 210 211 anchor.addEventListener("mouseover", (e) => { 212 // when CSS anchor positioning is supported, this is only used to set the font 213 let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`); 214 this.positionAnchor(anchor); 215 }); 216 217 return anchor; 218 } 219 220 get headings() { 221 return this.querySelectorAll(this.selector.split(",").map(entry => `${entry.trim()}[id]`)); 222 } 223 224 get selector() { 225 return this.getAttribute("selector") || HeadingAnchors.defaultSelector; 226 } 227} 228 229HeadingAnchors.register(); 230 231export { HeadingAnchors } 232const speakButton = document.getElementById('speak-button'); 233 const pauseButton = document.getElementById('pause-button'); 234 let isSpeaking = false;
235 236 // 1. Pre-load voices for Mobile Chrome 237 let voices = []; 238 window.speechSynthesis.onvoiceschanged = () => { 239 voices = window.speechSynthesis.getVoices(); 240 }; 241 242 speakButton.addEventListener('click', () => { 243 if (isSpeaking) { 244 window.speechSynthesis.cancel(); 245 resetUI(); 246 } else { 247 const title = document.getElementById('article-title')?.innerText || ""; 248 const content = document.getElementById('speak-content')?.innerText || ""; 249 const textToRead = title + ". " + content; 250 251 if (textToRead) { 252 // 2. CRITICAL FOR MOBILE: Cancel any stuck processes first 253 window.speechSynthesis.cancel(); 254 255 const utterance = new SpeechSynthesisUtterance(textToRead); 256 257 // 3. Set a default lang for mobile reliability 258 utterance.lang = 'en-US'; 259 utterance.rate = 0.9; 260 261 utterance.onend = () => resetUI(); 262 utterance.onerror = (event) => { 263 console.error('SpeechSynthesis Error:', event); 264 resetUI(); 265 }; 266 267 // 4. Speak 268 window.speechSynthesis.speak(utterance); 269 270 isSpeaking = true; 271 speakButton.innerHTML = "<span>ð</span> Stop"; 272 pauseButton.style.display = "flex"; 273 } 274 } 275 }); 276 277 pauseButton.addEventListener('click', () => { 278 // Note: resume() on mobile Chrome can be flaky. 279 // This is the standard way to handle it: 280 if (window.speechSynthesis.paused) { 281 window.speechSynthesis.resume(); 282 pauseButton.innerHTML = "<span>â¶ï¸</span> Resume"; 283 } else { 284 window.speechSynthesis.pause(); 285 pauseButton.innerHTML = "<span>â¸ï¸</span> Pause"; 286 } 287 }); 288 289 function resetUI() { 290 isSpeaking = false; 291 speakButton.innerHTML = "<span>ð</span> Listen"; 292 pauseButton.style.display = "none"; 293 pauseButton.innerHTML = "<span>â¸ï¸</span> Pause"; 294 } 295 296 window.onbeforeunload = () => window.speechSynthesis.cancel(); 297const shareBtn = document.getElementById('native-share-btn'); 298 if (navigator.share) { 299 shareBtn.style.display = 'flex'; 300 shareBtn.addEventListener('click', () => { 301 const articleTitle = document.querySelector('h1')?.innerText || "Rav Kook Torah"; 302 const shareUrl = window.location.href.split('#')[0]; 303 navigator.share({ 304 title: articleTitle, 305 text: articleTitle + "\n\n", 306 url: shareUrl 307 }).catch((error) => console.log('Error sharing', error)); 308 }); 309 } 310sefaria.link({ 311 mode: "popup-click" 312 }); 313(function (s, e, n, d, er) { 314 s['Sender'] = er; 315 s[er] = s[er] || function () { 316 (s[er].q = s[er].q || []).push(arguments) 317 }, s[er].l = 1 * new Date(); 318 var a = e.createElement(n), 319 m = e.getElementsByTagName(n)[0]; 320 a.async = 1; 321 a.src = d; 322 m.parentNode.insertBefore(a, m) 323 })(window, document, 'script', 'https://cdn.sender.net/accounts_resources/universal.js', 'sender'); 324 sender('422a8c94418565') 325window.addEventListener('DOMContentLoaded', (event) => { 326 const modal = document.getElementById("search-modal"); 327 const openBtn = document.getElementById("search-trigger"); 328 const closeBtn = document.getElementById("modal-close-btn"); 329 330 // Open Modal 331 if (openBtn) { 332 openBtn.onclick = (e) => { 333 e.preventDefault(); 334 modal.style.display = "block"; 335 }; 336 } 337 338 // Close Modal via 'X' button 339 if (closeBtn) { 340 closeBtn.onclick = () => { 341 modal.style.display = "none"; 342 }; 343 } 344 345 // Close Modal via clicking outside the box 346 window.onclick = (event) => { 347 if (event.target == modal) { 348 modal.style.display = "none"; 349 } 350 }; 351 352 // Close via Escape key 353 document.addEventListener('keydown', (e) => { 354 if (e.key === "Escape") modal.style.display = "none"; 355 }); 356 });
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.