Fork of Chiri for Astro for my blog
0
fork

Configure Feed

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

refactor: toc logic

the3ash 8190ec73 7967d74c

+137 -97
+137 -97
src/components/ui/TableOfContents.astro
··· 38 38 }} 39 39 > 40 40 ;(function () { 41 - let tocContainer = null 42 - let tocLinks = null 43 - let headings = null 44 - let titleLink = null 45 - const headingMap = new Map() 46 - let scrollTimeout = null 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 + } 47 52 48 - // Initialize DOM cache 49 - function initDOMCache() { 50 - tocContainer = document.querySelector('.toc-container') 51 - if (!tocContainer) return false 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 + }) 52 70 53 - tocLinks = document.querySelectorAll('.toc-link') 54 - headings = document.querySelectorAll('h1, h2, h3') 55 - titleLink = document.querySelector('.toc-link.toc-title') 56 - buildHeadingMap() 57 71 return true 58 72 } 59 73 60 - // Build heading map for efficient lookup 61 - function buildHeadingMap() { 62 - headingMap.clear() 63 - if (!tocLinks) return 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 64 91 65 - tocLinks.forEach((link) => { 66 - const href = link.getAttribute('href') 67 - if (href && href.startsWith('#')) { 68 - const id = href.substring(1) 69 - headingMap.set(id, link) 70 - } 71 - }) 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 + })) 72 99 } 73 100 74 - // Calculate TOC positioning 75 - function adjustTOCPosition() { 76 - if (!tocContainer || !centeredLayout) { 77 - if (tocContainer) tocContainer.style.display = 'none' 101 + // Adjust TOC position 102 + function adjustPosition() { 103 + if (!state.container || !centeredLayout || !state.hasContent) { 104 + if (state.container) state.container.style.display = 'none' 78 105 return 79 106 } 80 107 81 108 const pageWidth = window.innerWidth 82 - const contentWidthValue = parseFloat(contentWidth) 83 - const widthValue = Math.min(contentWidthValue, 50) 84 - const finalWidthValue = widthValue > 25 ? widthValue : 25 85 - const margin = (pageWidth - finalWidthValue * 16) / 2 86 - const minSpace = toc ? 216 : 176 // 11rem + 40px if toc enabled 109 + const contentWidthValue = Math.max(parseFloat(contentWidth), 25) 110 + const margin = (pageWidth - contentWidthValue * 16) / 2 111 + const minSpace = toc ? 216 : 176 87 112 88 113 if (margin >= minSpace) { 89 - tocContainer.style.display = 'block' 90 - tocContainer.classList.add('fixed-position') 114 + state.container.style.display = 'block' 115 + state.container.classList.add('fixed-position') 91 116 const leftPosition = toc ? margin - 176 - 40 : margin - 176 92 - tocContainer.style.left = `${leftPosition}px` 93 - setTimeout(updateActiveTOCItem, 100) 117 + state.container.style.left = `${leftPosition}px` 94 118 } else { 95 - tocContainer.style.display = 'none' 96 - tocContainer.classList.remove('fixed-position') 97 - tocContainer.style.left = '' 119 + state.container.style.display = 'none' 120 + state.container.classList.remove('fixed-position') 121 + state.container.style.left = '' 98 122 } 99 123 } 100 124 101 - // Check if TOC should be visible based on content 102 - function checkTOCVisibility() { 103 - if (!tocContainer || !tocLinks) return 104 - 105 - const tocItems = Array.from(tocLinks).filter((link) => !link.classList.contains('toc-title')) 106 - if (tocItems.length === 0) { 107 - tocContainer.style.display = 'none' 108 - } 109 - } 110 - 111 - // Handle TOC click events 112 - function handleTOCClick(e) { 125 + // Handle click events 126 + function handleClick(e) { 113 127 const link = e.target.closest('.toc-link') 114 128 if (!link) return 115 129 ··· 120 134 history.pushState(null, null, '#') 121 135 } else { 122 136 const href = link.getAttribute('href') 123 - if (href && href.startsWith('#')) { 124 - const targetId = href.substring(1) 125 - const target = document.getElementById(targetId) 137 + if (href?.startsWith('#')) { 138 + const target = document.getElementById(href.substring(1)) 126 139 if (target) { 127 140 const rect = target.getBoundingClientRect() 128 141 const scrollTop = window.pageYOffset || document.documentElement.scrollTop ··· 134 147 } 135 148 } 136 149 137 - // Update active TOC item based on scroll position 138 - function updateActiveTOCItem() { 139 - if (!tocLinks || !headings || tocLinks.length === 0 || headings.length === 0) { 140 - return 141 - } 150 + // Update active state 151 + function updateActive() { 152 + if (!state.links?.length || !state.positions.length) return 142 153 143 - let currentActive = null 144 154 const scrollTop = window.pageYOffset + 100 155 + let currentActive = null 145 156 146 - headings.forEach((heading, index) => { 147 - if (index === 0 && heading.tagName === 'H1') return 148 - 149 - const rect = heading.getBoundingClientRect() 150 - const offsetTop = rect.top + window.pageYOffset 151 - 152 - if (scrollTop >= offsetTop) { 153 - currentActive = heading.id 157 + // Find current active heading 158 + for (let i = state.positions.length - 1; i >= 0; i--) { 159 + if (scrollTop >= state.positions[i].offsetTop) { 160 + currentActive = state.positions[i].id 161 + break 154 162 } 155 - }) 163 + } 156 164 157 - tocLinks.forEach((link) => link.classList.remove('active')) 165 + // Update active state 166 + state.links.forEach((link) => link.classList.remove('active')) 158 167 159 - if (currentActive && headingMap.has(currentActive)) { 160 - const activeLink = headingMap.get(currentActive) 161 - activeLink.classList.add('active') 162 - } else if (titleLink) { 163 - titleLink.classList.add('active') 168 + if (currentActive && state.headingMap.has(currentActive)) { 169 + state.headingMap.get(currentActive).classList.add('active') 170 + } else if (state.titleLink) { 171 + state.titleLink.classList.add('active') 164 172 } 165 173 } 166 174 167 - // Initialize TOC 168 - function initializeTOC() { 169 - if (!initDOMCache()) { 170 - setTimeout(initializeTOC, 100) 171 - return 172 - } 175 + // Initialize 176 + function init(retryCount = 0) { 177 + const maxRetries = 5 173 178 174 - adjustTOCPosition() 175 - checkTOCVisibility() 179 + if (initElements()) { 180 + checkContent() 181 + adjustPosition() 182 + cachePositions() 176 183 177 - // Add event listeners 178 - tocContainer.removeEventListener('click', handleTOCClick) 179 - tocContainer.addEventListener('click', handleTOCClick) 184 + if (state.container) { 185 + state.container.removeEventListener('click', handleClick) 186 + state.container.addEventListener('click', handleClick) 187 + } 180 188 181 - updateActiveTOCItem() 189 + updateActive() 190 + } else if (retryCount < maxRetries) { 191 + setTimeout(() => init(retryCount + 1), 100) 192 + } 182 193 } 183 194 184 - // Debounced scroll handler 195 + // Event handlers 185 196 function handleScroll() { 186 - if (scrollTimeout) clearTimeout(scrollTimeout) 187 - scrollTimeout = setTimeout(updateActiveTOCItem, 16) 197 + if (state.scrollTimeout) { 198 + cancelAnimationFrame(state.scrollTimeout) 199 + } 200 + state.scrollTimeout = requestAnimationFrame(updateActive) 188 201 } 189 202 190 - // Initialize 191 - setTimeout(initializeTOC, 10) 203 + function handleResize() { 204 + adjustPosition() 205 + requestAnimationFrame(cachePositions) 206 + } 207 + 208 + // Cleanup 209 + function cleanup() { 210 + if (state.scrollTimeout) { 211 + cancelAnimationFrame(state.scrollTimeout) 212 + state.scrollTimeout = null 213 + } 214 + 215 + Object.assign(state, { 216 + container: null, 217 + links: null, 218 + headings: null, 219 + titleLink: null, 220 + headingMap: new Map(), 221 + positions: [], 222 + hasContent: false 223 + }) 224 + } 192 225 193 226 // Event listeners 194 227 document.addEventListener('astro:page-load', () => { 195 - tocContainer = null 196 - setTimeout(initializeTOC, 50) 228 + cleanup() 229 + init() 230 + }) 231 + 232 + document.addEventListener('astro:after-swap', () => { 233 + cleanup() 234 + init() 197 235 }) 198 236 199 - window.addEventListener('resize', adjustTOCPosition) 237 + window.addEventListener('resize', handleResize) 200 238 window.addEventListener('scroll', handleScroll) 201 239 })() 202 240 </script> ··· 208 246 left: -0.175em; 209 247 opacity: 0; 210 248 transition: opacity 0.2s ease-out; 249 + display: none; 211 250 } 212 251 213 252 .toc-container.fixed-position { ··· 238 277 margin-left: 0 !important; 239 278 padding-left: 0 !important; 240 279 } 280 + 241 281 .prose .toc-container .toc-list li { 242 282 margin: 0 !important; 243 283 padding: 0 !important;