Fork of Chiri for Astro for my blog
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a2f7dec4d7c309420bc9d4e87b4de27dcd8a4142 389 lines 9.7 kB view raw
1--- 2import { themeConfig } from '@/config' 3import type { TOCProps } from '@/types' 4 5const { toc = [] }: TOCProps = Astro.props 6--- 7 8<div class="toc-container" id="toc"> 9 <nav class="toc-nav"> 10 <ul class="toc-list" id="toc-list"> 11 <!-- Back to top link --> 12 <li class="toc-item toc-level-0"> 13 <a href="#" class="toc-link toc-title" title="Back to top" data-text="Back to top"> 14 Back to top 15 </a> 16 </li> 17 18 <!-- TOC items --> 19 { 20 toc.map((item) => ( 21 <li class={`toc-item toc-level-${item.level}`}> 22 <a href={`#${item.id}`} class="toc-link" title={item.text} data-text={item.text}> 23 {item.text} 24 </a> 25 </li> 26 )) 27 } 28 </ul> 29 </nav> 30</div> 31 32<script 33 is:inline 34 define:vars={{ 35 contentWidth: themeConfig.general.contentWidth, 36 centeredLayout: themeConfig.general.centeredLayout, 37 toc: themeConfig.post.toc 38 }} 39> 40 ;(function () { 41 // Core state 42 const state = { 43 container: null, 44 links: null, 45 headings: null, 46 titleLink: null, 47 headingMap: new Map(), 48 positions: [], 49 scrollTimeout: null, 50 hasContent: false 51 } 52 53 // Initialize DOM elements 54 function initElements() { 55 state.container = document.querySelector('.toc-container') 56 if (!state.container) return false 57 58 state.links = document.querySelectorAll('.toc-link') 59 state.headings = document.querySelectorAll('h1, h2, h3') 60 state.titleLink = document.querySelector('.toc-link.toc-title') 61 62 // Build heading map 63 state.headingMap.clear() 64 state.links.forEach((link) => { 65 const href = link.getAttribute('href') 66 if (href?.startsWith('#')) { 67 state.headingMap.set(href.substring(1), link) 68 } 69 }) 70 71 return true 72 } 73 74 // Check if content exists 75 function checkContent() { 76 if (!state.container || !state.links) return 77 78 const tocItems = Array.from(state.links).filter( 79 (link) => !link.classList.contains('toc-title') 80 ) 81 state.hasContent = tocItems.length > 0 82 83 if (!state.hasContent) { 84 state.container.style.display = 'none' 85 } 86 } 87 88 // Cache heading positions 89 function cachePositions() { 90 if (!state.headings?.length) return 91 92 const scrollTop = window.pageYOffset 93 state.positions = Array.from(state.headings) 94 .filter((_, index) => !(index === 0 && state.headings[0].tagName === 'H1')) 95 .map((heading) => ({ 96 id: heading.id, 97 offsetTop: heading.getBoundingClientRect().top + scrollTop 98 })) 99 } 100 101 // Adjust TOC position 102 function adjustPosition() { 103 if (!state.container || !centeredLayout || !state.hasContent) { 104 if (state.container) state.container.style.display = 'none' 105 return 106 } 107 108 const pageWidth = window.innerWidth 109 const contentWidthValue = Math.max(parseFloat(contentWidth), 25) 110 const margin = (pageWidth - contentWidthValue * 16) / 2 111 const baseMinSpace = 11 * 16 // Base minimum space needed 112 const minSpace = toc ? baseMinSpace + 52 : baseMinSpace + 12 113 114 if (margin >= minSpace) { 115 state.container.style.display = 'block' 116 state.container.classList.add('fixed-position') 117 const leftPosition = toc ? margin - 176 - 40 : margin - 176 118 state.container.style.left = `${leftPosition}px` 119 } else { 120 state.container.style.display = 'none' 121 state.container.classList.remove('fixed-position') 122 state.container.style.left = '' 123 } 124 } 125 126 // Handle click events 127 function handleClick(e) { 128 const link = e.target.closest('.toc-link') 129 if (!link) return 130 131 e.preventDefault() 132 133 if (link.classList.contains('toc-title')) { 134 window.scrollTo({ top: 0, behavior: 'smooth' }) 135 history.pushState(null, null, '#') 136 } else { 137 const href = link.getAttribute('href') 138 if (href?.startsWith('#')) { 139 const target = document.getElementById(href.substring(1)) 140 if (target) { 141 const rect = target.getBoundingClientRect() 142 const scrollTop = window.pageYOffset || document.documentElement.scrollTop 143 const offset = rect.top + scrollTop - 96 144 window.scrollTo({ top: offset, behavior: 'smooth' }) 145 history.pushState(null, null, href) 146 } 147 } 148 } 149 } 150 151 // Update active state 152 function updateActive() { 153 if (!state.links?.length || !state.positions.length) return 154 155 const scrollTop = window.pageYOffset + 100 156 let currentActive = null 157 158 // Find current active heading 159 for (let i = state.positions.length - 1; i >= 0; i--) { 160 if (scrollTop >= state.positions[i].offsetTop) { 161 currentActive = state.positions[i].id 162 break 163 } 164 } 165 166 // Update active state 167 state.links.forEach((link) => link.classList.remove('active')) 168 169 if (currentActive && state.headingMap.has(currentActive)) { 170 state.headingMap.get(currentActive).classList.add('active') 171 } else if (state.titleLink) { 172 state.titleLink.classList.add('active') 173 } 174 } 175 176 // Initialize 177 function init(retryCount = 0) { 178 const maxRetries = 5 179 180 if (initElements()) { 181 checkContent() 182 adjustPosition() 183 cachePositions() 184 185 if (state.container) { 186 state.container.removeEventListener('click', handleClick) 187 state.container.addEventListener('click', handleClick) 188 } 189 190 updateActive() 191 } else if (retryCount < maxRetries) { 192 setTimeout(() => init(retryCount + 1), 100) 193 } 194 } 195 196 // Event handlers 197 function handleScroll() { 198 if (state.scrollTimeout) { 199 cancelAnimationFrame(state.scrollTimeout) 200 } 201 state.scrollTimeout = requestAnimationFrame(updateActive) 202 } 203 204 function handleResize() { 205 adjustPosition() 206 requestAnimationFrame(cachePositions) 207 } 208 209 // Cleanup 210 function cleanup() { 211 if (state.scrollTimeout) { 212 cancelAnimationFrame(state.scrollTimeout) 213 state.scrollTimeout = null 214 } 215 216 Object.assign(state, { 217 container: null, 218 links: null, 219 headings: null, 220 titleLink: null, 221 headingMap: new Map(), 222 positions: [], 223 hasContent: false 224 }) 225 } 226 227 // Event listeners 228 document.addEventListener('astro:page-load', () => { 229 cleanup() 230 init() 231 }) 232 233 document.addEventListener('astro:after-swap', () => { 234 cleanup() 235 init() 236 }) 237 238 // Fallback for when Astro transitions are disabled 239 document.addEventListener('DOMContentLoaded', () => { 240 if (!state.container || !state.hasContent) { 241 init() 242 } 243 }) 244 245 window.addEventListener('resize', handleResize) 246 window.addEventListener('scroll', handleScroll) 247 })() 248</script> 249 250<style is:inline> 251 .toc-container { 252 width: 12rem; 253 position: relative; 254 left: -0.175em; 255 opacity: 0; 256 transition: opacity 0.2s ease-out; 257 display: none; 258 } 259 260 .toc-container.fixed-position { 261 opacity: 1; 262 position: fixed; 263 top: 12rem; 264 margin-top: 0; 265 padding-left: 1rem; 266 z-index: 10; 267 left: auto; 268 } 269 270 .toc-nav { 271 font-family: var(--sans); 272 } 273 274 .toc-list, 275 .toc-list li, 276 .toc-item { 277 list-style: none; 278 margin: 0; 279 padding: 0; 280 } 281 282 .prose .toc-container .toc-list { 283 margin-left: 0 !important; 284 padding-left: 0 !important; 285 } 286 287 .prose .toc-container .toc-list li { 288 margin: 0 !important; 289 padding: 0 !important; 290 } 291 292 .toc-item::before, 293 .toc-item::marker { 294 display: none; 295 } 296 297 .toc-link { 298 display: block; 299 color: transparent; 300 text-decoration: none; 301 position: relative; 302 padding-left: 0; 303 height: 1.125rem; 304 width: 100%; 305 min-height: 1rem; 306 font-size: 0; 307 line-height: 1.125rem; 308 text-indent: 2rem; 309 overflow: hidden; 310 white-space: nowrap; 311 text-overflow: ellipsis; 312 transition: 313 color 0.2s ease-out, 314 font-size 0.2s ease-out, 315 text-indent 0.2s ease-out; 316 cursor: pointer; 317 } 318 319 .toc-link::after { 320 content: attr(data-text); 321 position: absolute; 322 left: -0.5rem; 323 top: 0; 324 font-family: var(--sans); 325 font-size: var(--font-size-s); 326 letter-spacing: var(--spacing-m); 327 line-height: 1.125rem; 328 color: var(--text-primary); 329 opacity: 0; 330 transition: 331 opacity 0.2s ease-out, 332 left 0.2s ease-out; 333 pointer-events: none; 334 overflow: hidden; 335 white-space: nowrap; 336 text-overflow: ellipsis; 337 max-width: 100%; 338 } 339 340 .toc-link:hover::after { 341 opacity: 1; 342 left: -0.75rem; 343 } 344 345 .toc-level-0 .toc-link:hover::after { 346 opacity: 0; 347 } 348 349 .toc-level-1 .toc-link:hover::before, 350 .toc-level-2 .toc-link:hover::before, 351 .toc-level-3 .toc-link:hover::before { 352 width: 0.75rem; 353 transition: width 0.1s ease-out; 354 } 355 356 .toc-link.active { 357 color: var(--text-primary); 358 } 359 360 /* Horizontal line indicators */ 361 .toc-level-0 .toc-link::before, 362 .toc-level-1 .toc-link::before, 363 .toc-level-2 .toc-link::before, 364 .toc-level-3 .toc-link::before { 365 content: ''; 366 position: absolute; 367 left: 0; 368 top: 50%; 369 width: 1.75rem; 370 height: 1px; 371 background-color: var(--text-tertiary); 372 transform: translateY(-50%); 373 opacity: 0.4; 374 transition: all 0.1s ease-out; 375 } 376 377 .toc-link:hover::before, 378 .toc-link.active::before { 379 opacity: 0.8; 380 background-color: var(--text-primary); 381 } 382 383 /* Hide on mobile */ 384 @media (max-width: 768px) { 385 .toc-container { 386 display: none !important; 387 } 388 } 389</style>