미디어위키:Gadget-ArticleToc.js

식물 vs 좀비 위키
Brokey (토론 | 기여)님의 2026년 7월 30일 (목) 21:19 판
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)
둘러보기로 이동 검색으로 이동

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
/* MediaWiki:Gadget-ArticleToc.js
 * 데스크톱/모바일 공통 목차 위치 보정 및 자동 생성
 */

(function () {
	'use strict';

	function animateList(list, expand) {
		var startHeight = list.getBoundingClientRect().height;
		var endHeight;
		var finished = false;
		var timer;

		function finish() {
			if (finished) {
				return;
			}

			finished = true;
			window.clearTimeout(timer);
			list.style.height = expand ? 'auto' : '0px';
			list.style.pointerEvents = expand ? '' : 'none';
			list.setAttribute('aria-hidden', String(!expand));
			list.removeEventListener('transitionend', onTransitionEnd);
		}

		function onTransitionEnd(event) {
			if (event.propertyName === 'height') {
				finish();
			}
		}

		list.style.height = startHeight + 'px';
		list.style.pointerEvents = '';
		list.offsetHeight;

		endHeight = expand ? list.scrollHeight : 0;
		list.addEventListener('transitionend', onTransitionEnd);
		list.style.height = endHeight + 'px';

		timer = window.setTimeout(finish, 320);
	}

	function installNativeTocAnimation(toc) {
		var checkbox = toc.querySelector('.toctogglecheckbox');
		var list = toc.querySelector(':scope > ul');

		if (!checkbox || !list || checkbox.dataset.pvzTocAnimation === '1') {
			return;
		}

		checkbox.dataset.pvzTocAnimation = '1';
		list.style.height = checkbox.checked ? '0px' : 'auto';
		list.style.pointerEvents = checkbox.checked ? 'none' : '';
		list.setAttribute('aria-hidden', String(checkbox.checked));

		checkbox.addEventListener('change', function () {
			animateList(list, !checkbox.checked);
		});
	}

	function getHeadingText(heading) {
		var copy = heading.cloneNode(true);
		var editLinks = copy.querySelectorAll(
			'.mw-editsection, .mw-editsection-like'
		);

		Array.prototype.forEach.call(editLinks, function (editLink) {
			editLink.remove();
		});

		return copy.textContent.replace(/\s+/g, ' ').trim();
	}

	function getHeadings(output) {
		var headings = output.querySelectorAll('h2, h3, h4, h5, h6');

		return Array.prototype.filter.call(headings, function (heading) {
			return !heading.closest(
				'#toc, .toc, .mw-table-of-contents, .pvz-generated-toc'
			);
		});
	}

	function getHeadingBlock(heading) {
		return heading.closest(
			'.mw-heading, .mw-heading2, .mw-heading3, ' +
			'.mw-heading4, .mw-heading5, .mw-heading6'
		) || heading;
	}

	function moveNativeToc(output, firstHeading) {
		var toc = output.querySelector(
			'#toc, .toc, .mw-table-of-contents'
		);

		if (!toc) {
			return null;
		}

		toc.classList.add('pvz-custom-toc');
		installNativeTocAnimation(toc);

		if (firstHeading) {
			firstHeading.parentNode.insertBefore(
				toc,
				getHeadingBlock(firstHeading)
			);
		}

		return toc;
	}

	function createGeneratedToc(output, headings) {
		var existing = output.querySelector('.pvz-generated-toc');
		var toc;
		var header;
		var title;
		var button;
		var list;
		var counters = [0, 0, 0, 0, 0, 0, 0];
		var baseLevel;

		if (existing || !headings.length) {
			return existing;
		}

		baseLevel = headings.reduce(function (lowest, heading) {
			var level = Number(heading.tagName.slice(1));
			return Math.min(lowest, level);
		}, 6);

		toc = document.createElement('nav');
		toc.id = 'pvz-generated-toc';
		toc.className = 'toc pvz-custom-toc pvz-generated-toc';
		toc.setAttribute('role', 'navigation');
		toc.setAttribute('aria-labelledby', 'pvz-generated-toc-heading');

		header = document.createElement('div');
		header.className = 'toctitle pvz-toc-header';

		title = document.createElement('h2');
		title.id = 'pvz-generated-toc-heading';
		title.textContent = '목차';

		button = document.createElement('button');
		button.type = 'button';
		button.className = 'pvz-toc-toggle';
		button.textContent = '접기';
		button.setAttribute('aria-expanded', 'true');
		button.setAttribute('aria-controls', 'pvz-generated-toc-list');

		list = document.createElement('ul');
		list.id = 'pvz-generated-toc-list';
		list.className = 'pvz-toc-list';

		headings.forEach(function (heading, index) {
			var level = Number(heading.tagName.slice(1));
			var text = getHeadingText(heading);
			var item;
			var link;
			var number;
			var label;
			var numberText;

			if (!text) {
				return;
			}

			if (!heading.id) {
				heading.id = 'pvz-section-' + (index + 1);
			}

			counters[level] += 1;

			for (var deeper = level + 1; deeper <= 6; deeper += 1) {
				counters[deeper] = 0;
			}

			numberText = counters
				.slice(baseLevel, level + 1)
				.filter(function (value) {
					return value > 0;
				})
				.join('.');

			item = document.createElement('li');
			item.className = 'pvz-toc-item toclevel-' + level;
			item.style.setProperty(
				'--pvz-toc-depth',
				String(Math.max(0, level - baseLevel))
			);

			link = document.createElement('a');
			link.href = '#' + heading.id;

			number = document.createElement('span');
			number.className = 'tocnumber pvz-toc-number';
			number.textContent = numberText;

			label = document.createElement('span');
			label.className = 'toctext pvz-toc-text';
			label.textContent = text;

			link.appendChild(number);
			link.appendChild(label);
			item.appendChild(link);
			list.appendChild(item);
		});

		if (!list.children.length) {
			return null;
		}

		button.addEventListener('click', function () {
			var isOpen = button.getAttribute('aria-expanded') === 'true';

			button.setAttribute('aria-expanded', String(!isOpen));
			button.textContent = isOpen ? '펼치기' : '접기';
			animateList(list, !isOpen);
		});

		header.appendChild(title);
		header.appendChild(button);
		toc.appendChild(header);
		toc.appendChild(list);

		getHeadingBlock(headings[0]).parentNode.insertBefore(
			toc,
			getHeadingBlock(headings[0])
		);

		return toc;
	}

	function installArticleToc(root) {
		var scope = root && root.querySelector ? root : document;
		var output = scope.matches && scope.matches('.mw-parser-output') ?
			scope :
			scope.querySelector('.mw-parser-output');
		var headings;
		var nativeToc;

		if (!output) {
			return;
		}

		headings = getHeadings(output);

		if (!headings.length) {
			return;
		}

		nativeToc = moveNativeToc(output, headings[0]);

		if (!nativeToc) {
			createGeneratedToc(output, headings);
		}
	}

	function init() {
		installArticleToc(document);

		if (window.mw && mw.hook) {
			mw.hook('wikipage.content').add(function ($content) {
				var root = $content && $content[0] ?
					$content[0] :
					document;

				installArticleToc(root);
			});
		}
	}

	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init, { once: true });
	} else {
		init();
	}
}());