|
| 1 | + |
| 2 | +(function () { |
| 3 | + 'use strict'; |
| 4 | + |
| 5 | + function initReadingProgress() { |
| 6 | + const bar = document.createElement('div'); |
| 7 | + bar.id = 'reading-progress'; |
| 8 | + bar.setAttribute('role', 'progressbar'); |
| 9 | + bar.setAttribute('aria-label', '읽기 진행률'); |
| 10 | + bar.setAttribute('aria-valuemin', '0'); |
| 11 | + bar.setAttribute('aria-valuemax', '100'); |
| 12 | + document.body.prepend(bar); |
| 13 | + |
| 14 | + function updateProgress() { |
| 15 | + const doc = document.documentElement; |
| 16 | + const scrollTop = doc.scrollTop || document.body.scrollTop; |
| 17 | + const scrollHeight = doc.scrollHeight - doc.clientHeight; |
| 18 | + const progress = scrollHeight > 0 ? Math.round((scrollTop / scrollHeight) * 100) : 0; |
| 19 | + bar.style.width = progress + '%'; |
| 20 | + bar.setAttribute('aria-valuenow', progress); |
| 21 | + } |
| 22 | + |
| 23 | + window.addEventListener('scroll', updateProgress, { passive: true }); |
| 24 | + updateProgress(); |
| 25 | + } |
| 26 | + |
| 27 | + function initSkipToContent() { |
| 28 | + if (document.getElementById('skip-to-content')) return; |
| 29 | + const link = document.createElement('a'); |
| 30 | + link.id = 'skip-to-content'; |
| 31 | + link.href = '#main-content'; |
| 32 | + link.className = 'skip-to-content'; |
| 33 | + link.textContent = '본문으로 건너뛰기'; |
| 34 | + |
| 35 | + const main = document.querySelector('.md-main__inner, .md-content, main'); |
| 36 | + if (main && !main.id) { |
| 37 | + main.id = 'main-content'; |
| 38 | + } |
| 39 | + |
| 40 | + document.body.prepend(link); |
| 41 | + } |
| 42 | + |
| 43 | + function initExternalLinkIndicators() { |
| 44 | + document.querySelectorAll('.md-typeset a[href]').forEach(function (a) { |
| 45 | + const href = a.getAttribute('href') || ''; |
| 46 | + const isExternal = |
| 47 | + href.startsWith('http') && |
| 48 | + !href.includes(window.location.hostname) && |
| 49 | + !href.includes('docs.nodove.com'); |
| 50 | + |
| 51 | + if (isExternal) { |
| 52 | + if (!a.getAttribute('target')) { |
| 53 | + a.setAttribute('target', '_blank'); |
| 54 | + a.setAttribute('rel', 'noopener noreferrer'); |
| 55 | + } |
| 56 | + if (!a.querySelector('.external-icon') && !a.closest('.md-button')) { |
| 57 | + const icon = document.createElement('span'); |
| 58 | + icon.className = 'external-icon'; |
| 59 | + icon.setAttribute('aria-label', '(외부 링크)'); |
| 60 | + icon.innerHTML = |
| 61 | + '<svg xmlns="http://www.w3.org/2000/svg" width="0.75em" height="0.75em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>'; |
| 62 | + icon.style.cssText = |
| 63 | + 'display:inline-block;margin-left:0.2em;vertical-align:middle;opacity:0.6;'; |
| 64 | + a.appendChild(icon); |
| 65 | + } |
| 66 | + } |
| 67 | + }); |
| 68 | + } |
| 69 | + |
| 70 | + function initTableOfContentsHighlight() { |
| 71 | + const tocLinks = document.querySelectorAll('.md-nav--secondary .md-nav__link'); |
| 72 | + if (!tocLinks.length) return; |
| 73 | + |
| 74 | + const headings = Array.from( |
| 75 | + document.querySelectorAll('.md-content h2[id], .md-content h3[id]') |
| 76 | + ); |
| 77 | + |
| 78 | + if (!headings.length) return; |
| 79 | + |
| 80 | + const observer = new IntersectionObserver( |
| 81 | + function (entries) { |
| 82 | + entries.forEach(function (entry) { |
| 83 | + if (entry.isIntersecting) { |
| 84 | + const id = entry.target.id; |
| 85 | + tocLinks.forEach(function (link) { |
| 86 | + const href = link.getAttribute('href'); |
| 87 | + if (href === '#' + id) { |
| 88 | + link.classList.add('md-nav__link--active-scroll'); |
| 89 | + } else { |
| 90 | + link.classList.remove('md-nav__link--active-scroll'); |
| 91 | + } |
| 92 | + }); |
| 93 | + } |
| 94 | + }); |
| 95 | + }, |
| 96 | + { |
| 97 | + rootMargin: '-10% 0px -80% 0px', |
| 98 | + threshold: 0, |
| 99 | + } |
| 100 | + ); |
| 101 | + |
| 102 | + headings.forEach(function (h) { |
| 103 | + observer.observe(h); |
| 104 | + }); |
| 105 | + } |
| 106 | + |
| 107 | + function initKeyboardNav() { |
| 108 | + document.addEventListener('keydown', function (e) { |
| 109 | + if (e.key === '/' && !e.ctrlKey && !e.metaKey) { |
| 110 | + const activeEl = document.activeElement; |
| 111 | + const isInput = |
| 112 | + activeEl && |
| 113 | + (activeEl.tagName === 'INPUT' || |
| 114 | + activeEl.tagName === 'TEXTAREA' || |
| 115 | + activeEl.isContentEditable); |
| 116 | + if (!isInput) { |
| 117 | + const searchInput = document.querySelector('.md-search__input'); |
| 118 | + if (searchInput) { |
| 119 | + e.preventDefault(); |
| 120 | + searchInput.focus(); |
| 121 | + searchInput.select(); |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + }); |
| 126 | + } |
| 127 | + |
| 128 | + function initSmoothTabs() { |
| 129 | + const tabLinks = document.querySelectorAll('.md-tabs__link'); |
| 130 | + tabLinks.forEach(function (link) { |
| 131 | + if (!link.getAttribute('tabindex')) { |
| 132 | + link.setAttribute('tabindex', '0'); |
| 133 | + } |
| 134 | + if (!link.getAttribute('aria-label')) { |
| 135 | + const text = link.textContent.trim(); |
| 136 | + if (text) link.setAttribute('aria-label', text + ' 섹션'); |
| 137 | + } |
| 138 | + }); |
| 139 | + } |
| 140 | + |
| 141 | + function init() { |
| 142 | + initReadingProgress(); |
| 143 | + initSkipToContent(); |
| 144 | + initKeyboardNav(); |
| 145 | + initSmoothTabs(); |
| 146 | + |
| 147 | + document$.subscribe(function () { |
| 148 | + initExternalLinkIndicators(); |
| 149 | + initTableOfContentsHighlight(); |
| 150 | + }); |
| 151 | + } |
| 152 | + |
| 153 | + if (typeof document$ !== 'undefined') { |
| 154 | + document$.subscribe(init); |
| 155 | + } else if (document.readyState === 'loading') { |
| 156 | + document.addEventListener('DOMContentLoaded', init); |
| 157 | + } else { |
| 158 | + init(); |
| 159 | + } |
| 160 | +})(); |
0 commit comments