- 3개 백과사전 페이지 공용 glossary-tooltip 컴포넌트 - JS TreeWalker 자동 감지 + CSS-only 풍선 툴팁 - 도메인별 용어사전: 방화셔터/IT기획/서버지식 각 25~30개
166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
/**
|
|
* Academy Glossary Tooltip
|
|
*
|
|
* 페이지 로드 후 콘텐츠 영역의 텍스트를 스캔하여
|
|
* 용어사전 매칭 → <span class="glossary-term"> 래핑
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var DOMAIN = window.__GLOSSARY_DOMAIN;
|
|
var DATA = window.__GLOSSARY_DATA;
|
|
|
|
if (!DOMAIN || !DATA || !DATA[DOMAIN]) return;
|
|
|
|
var glossary = DATA[DOMAIN];
|
|
|
|
// 용어 목록: 길이 내림차순 정렬 (긴 용어 우선 매칭)
|
|
var terms = Object.keys(glossary).sort(function (a, b) {
|
|
return b.length - a.length;
|
|
});
|
|
|
|
if (terms.length === 0) return;
|
|
|
|
// 정규식 특수문자 이스케이프
|
|
function escapeRegex(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&');
|
|
}
|
|
|
|
// 단일 정규식 생성
|
|
var pattern = new RegExp(
|
|
terms.map(escapeRegex).join('|'),
|
|
'g'
|
|
);
|
|
|
|
// 이미 래핑된 용어 추적 (페이지당 첫 등장만)
|
|
var matched = {};
|
|
|
|
// 건너뛸 태그
|
|
var SKIP_TAGS = {
|
|
SCRIPT: true, STYLE: true, CODE: true, PRE: true,
|
|
NAV: true, TEXTAREA: true, INPUT: true, SELECT: true
|
|
};
|
|
|
|
/**
|
|
* TreeWalker로 텍스트 노드를 순회하며 용어 래핑
|
|
*/
|
|
function processContent() {
|
|
var container = document.querySelector('.academy-content')
|
|
|| document.querySelector('[class*="max-w-"]')
|
|
|| document.querySelector('main')
|
|
|| document.body;
|
|
|
|
var walker = document.createTreeWalker(
|
|
container,
|
|
NodeFilter.SHOW_TEXT,
|
|
{
|
|
acceptNode: function (node) {
|
|
// 이미 래핑된 용어 내부 건너뛰기
|
|
var parent = node.parentElement;
|
|
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
if (parent.classList && parent.classList.contains('glossary-term')) {
|
|
return NodeFilter.FILTER_REJECT;
|
|
}
|
|
// 건너뛸 태그 확인
|
|
var el = parent;
|
|
while (el) {
|
|
if (SKIP_TAGS[el.tagName]) return NodeFilter.FILTER_REJECT;
|
|
el = el.parentElement;
|
|
}
|
|
return NodeFilter.FILTER_ACCEPT;
|
|
}
|
|
}
|
|
);
|
|
|
|
var textNodes = [];
|
|
var node;
|
|
while ((node = walker.nextNode())) {
|
|
textNodes.push(node);
|
|
}
|
|
|
|
textNodes.forEach(function (textNode) {
|
|
var text = textNode.nodeValue;
|
|
if (!text || !text.trim()) return;
|
|
|
|
// 패턴 매칭 확인
|
|
pattern.lastIndex = 0;
|
|
if (!pattern.test(text)) return;
|
|
|
|
// 실제 치환
|
|
pattern.lastIndex = 0;
|
|
var frag = document.createDocumentFragment();
|
|
var lastIndex = 0;
|
|
var match;
|
|
|
|
while ((match = pattern.exec(text)) !== null) {
|
|
var term = match[0];
|
|
|
|
// 첫 등장만 래핑
|
|
if (matched[term]) {
|
|
continue;
|
|
}
|
|
matched[term] = true;
|
|
|
|
// 매칭 앞 텍스트
|
|
if (match.index > lastIndex) {
|
|
frag.appendChild(
|
|
document.createTextNode(text.slice(lastIndex, match.index))
|
|
);
|
|
}
|
|
|
|
// 용어 span 생성
|
|
var span = document.createElement('span');
|
|
span.className = 'glossary-term';
|
|
span.setAttribute('data-tooltip', glossary[term]);
|
|
span.textContent = term;
|
|
frag.appendChild(span);
|
|
|
|
lastIndex = match.index + term.length;
|
|
}
|
|
|
|
// 치환이 없었으면 건너뛰기
|
|
if (lastIndex === 0) return;
|
|
|
|
// 남은 텍스트
|
|
if (lastIndex < text.length) {
|
|
frag.appendChild(
|
|
document.createTextNode(text.slice(lastIndex))
|
|
);
|
|
}
|
|
|
|
textNode.parentNode.replaceChild(frag, textNode);
|
|
});
|
|
}
|
|
|
|
// 모바일 터치 지원
|
|
function setupMobileTouch() {
|
|
document.addEventListener('click', function (e) {
|
|
var term = e.target.closest('.glossary-term');
|
|
|
|
// 다른 곳 클릭 시 모든 활성 툴팁 제거
|
|
var actives = document.querySelectorAll('.glossary-term.is-tooltip-active');
|
|
actives.forEach(function (el) {
|
|
if (el !== term) el.classList.remove('is-tooltip-active');
|
|
});
|
|
|
|
// 용어 클릭 시 토글
|
|
if (term) {
|
|
e.preventDefault();
|
|
term.classList.toggle('is-tooltip-active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// 실행
|
|
function init() {
|
|
processContent();
|
|
setupMobileTouch();
|
|
}
|
|
|
|
if ('requestIdleCallback' in window) {
|
|
requestIdleCallback(init, { timeout: 2000 });
|
|
} else {
|
|
setTimeout(init, 100);
|
|
}
|
|
})();
|