미디어위키:Common.js

신사 (토론 | 기여)님의 2026년 8월 20일 (목) 13:26 판

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

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
/**
 * MediaWiki 커스텀 펼치기/접기 애니메이션
 *
 * 틀 구조 수정 불필요.
 *
 * 지원 구조:
 *
 * <div id="mw-customcollapsible-XXX" class="mw-collapsible">
 *     <div class="mw-customtoggle-XXX mw-customtoggle">
 *         [ 펼치기 · 접기 ]
 *     </div>
 *
 *     <table>...</table>
 * </div>
 *
 * 토글 버튼은 항상 남겨두고
 * 버튼 뒤의 내용만 슬라이드합니다.
 */

( function () {
	'use strict';

	var DURATION = 300;

	function getToggleName( toggle ) {
		var prefix = 'mw-customtoggle-';
		var classes = toggle.classList;
		var i;

		for ( i = 0; i < classes.length; i++ ) {
			if ( classes[ i ].indexOf( prefix ) === 0 ) {
				return classes[ i ].substring( prefix.length );
			}
		}

		return null;
	}

	function getTarget( toggle ) {
		var name = getToggleName( toggle );

		if ( !name ) {
			return null;
		}

		return document.getElementById(
			'mw-customcollapsible-' + name
		);
	}

	/*
	 * 토글 버튼 다음에 있는 실제 내용들을 찾음.
	 *
	 * 여성 아나운서처럼 같은 div 안에 table이 있는 경우 대응.
	 */
	function getInternalContents( target, toggle ) {
		var contents = [];
		var node = toggle.nextElementSibling;

		while ( node ) {
			contents.push( node );
			node = node.nextElementSibling;
		}

		return contents;
	}

	/*
	 * 남성 아나운서처럼 target 바깥 바로 다음에
	 * table이 있는 경우를 위한 처리.
	 */
	function getExternalContents( target ) {
		var contents = [];
		var node = target.nextElementSibling;

		while ( node ) {
			/*
			 * 다음 펼치기/접기 영역이 시작되면 종료
			 */
			if (
				node.id &&
				node.id.indexOf( 'mw-customcollapsible-' ) === 0
			) {
				break;
			}

			contents.push( node );

			/*
			 * 현재 구조에서는 표 하나면 충분하므로
			 * 첫 실제 요소만 대상으로 함
			 */
			break;
		}

		return contents;
	}

	function getContents( target, toggle ) {
		var internal = getInternalContents(
			target,
			toggle
		);

		if ( internal.length ) {
			return internal;
		}

		return getExternalContents( target );
	}

	function isOpen( toggle, contents ) {
		if (
			toggle.getAttribute( 'aria-expanded' ) ===
			'false'
		) {
			return false;
		}

		if ( !contents.length ) {
			return false;
		}

		return (
			window.getComputedStyle(
				contents[ 0 ]
			).display !== 'none'
		);
	}

	/*
	 * 펼치기
	 */
	function openContents( toggle, contents ) {
		toggle.setAttribute(
			'aria-expanded',
			'true'
		);

		contents.forEach( function ( element ) {
			if ( element._collapseAnimation ) {
				element._collapseAnimation.cancel();
			}

			/*
			 * 먼저 원래 display 복구
			 */
			element.style.display = '';

			/*
			 * CSS 때문에 여전히 none인 경우
			 * 태그에 맞는 display 사용
			 */
			if (
				window.getComputedStyle( element ).display ===
				'none'
			) {
				if ( element.tagName === 'TABLE' ) {
					element.style.display = 'table';
				} else {
					element.style.display = 'block';
				}
			}

			var endHeight = element.scrollHeight;

			element.style.overflow = 'hidden';

			var animation = element.animate(
				[
					{
						height: '0px'
					},
					{
						height: endHeight + 'px'
					}
				],
				{
					duration: DURATION,
					easing: 'ease',
					fill: 'forwards'
				}
			);

			element._collapseAnimation =
				animation;

			animation.onfinish = function () {
				element.style.height = '';
				element.style.overflow = '';
				element.style.display = '';

				element._collapseAnimation =
					null;

				animation.cancel();
			};
		} );
	}

	/*
	 * 접기
	 */
	function closeContents( toggle, contents ) {
		toggle.setAttribute(
			'aria-expanded',
			'false'
		);

		contents.forEach( function ( element ) {
			if ( element._collapseAnimation ) {
				element._collapseAnimation.cancel();
			}

			var startHeight =
				element.getBoundingClientRect().height;

			if ( startHeight <= 0 ) {
				element.style.display = 'none';
				return;
			}

			element.style.overflow = 'hidden';

			var animation = element.animate(
				[
					{
						height:
							startHeight + 'px'
					},
					{
						height: '0px'
					}
				],
				{
					duration: DURATION,
					easing: 'ease',
					fill: 'forwards'
				}
			);

			element._collapseAnimation =
				animation;

			animation.onfinish = function () {
				element.style.display = 'none';
				element.style.height = '';
				element.style.overflow = '';

				element._collapseAnimation =
					null;

				animation.cancel();
			};
		} );
	}

	function handleClick( event ) {
		var toggle = event.currentTarget;
		var target = getTarget( toggle );

		if ( !target ) {
			return;
		}

		var contents = getContents(
			target,
			toggle
		);

		/*
		 * 대상을 못 찾았다면 MediaWiki 기본 기능을
		 * 막지 않고 그대로 둔다.
		 */
		if ( !contents.length ) {
			return;
		}

		/*
		 * 이제 대상이 확실하게 있을 때만
		 * MediaWiki 기본 즉시 접기/펼치기를 막음.
		 */
		event.preventDefault();
		event.stopPropagation();
		event.stopImmediatePropagation();

		if ( isOpen( toggle, contents ) ) {
			closeContents(
				toggle,
				contents
			);
		} else {
			openContents(
				toggle,
				contents
			);
		}
	}

	function handleKeydown( event ) {
		if (
			event.key !== 'Enter' &&
			event.key !== ' '
		) {
			return;
		}

		handleClick( event );
	}

	function initialize() {
		var toggles =
			document.querySelectorAll(
				'.mw-customtoggle'
			);

		toggles.forEach( function ( toggle ) {
			if (
				!getToggleName( toggle ) ||
				toggle.dataset.collapseAnimationReady === '1'
			) {
				return;
			}

			toggle.dataset.collapseAnimationReady =
				'1';

			/*
			 * document capture가 아니라
			 * 실제 버튼 자체에 직접 등록
			 */
			toggle.addEventListener(
				'click',
				handleClick
			);

			toggle.addEventListener(
				'keydown',
				handleKeydown
			);
		} );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener(
			'DOMContentLoaded',
			initialize
		);
	} else {
		initialize();
	}

	if ( window.mw && mw.hook ) {
		mw.hook( 'wikipage.content' ).add(
			function () {
				initialize();
			}
		);
	}

}() );