미디어위키:Common.js

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

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

  • 파이어폭스 / 사파리: 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>
 *
 *     실제 내용...
 *
 * </div>
 *
 * 토글 버튼은 항상 남겨두고
 * 토글 버튼 뒤의 내용만 애니메이션합니다.
 */

( function () {
	'use strict';

	var DURATION = 300;

	var BODY_CLASS = 'mw-customcollapse-animation-body';
	var READY_CLASS = 'mw-customcollapse-animation-ready';
	var OPEN_CLASS = 'mw-customcollapse-animation-open';
	var ANIMATING_CLASS = 'mw-customcollapse-animation-running';


	/**
	 * 토글 이름 추출
	 *
	 * mw-customtoggle-aurann_01
	 * →
	 * aurann_01
	 */
	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;
		}

		/*
		 * 현재 구조에서는 toggle 자체가
		 * collapsible 안에 있으므로 우선 closest 사용
		 */
		var target = toggle.closest(
			'.mw-collapsible'
		);

		if (
			target &&
			target.id ===
				'mw-customcollapsible-' + name
		) {
			return target;
		}

		/*
		 * 혹시 구조가 다를 경우 ID로 검색
		 */
		return document.getElementById(
			'mw-customcollapsible-' + name
		);
	}


	/**
	 * 토글 버튼 이후의 모든 내용을
	 * 애니메이션용 DIV로 감싼다.
	 *
	 * 틀 원본 HTML은 수정할 필요 없음.
	 */
	function createAnimationBody( target, toggle ) {
		var existing = target.querySelector(
			':scope > .' + BODY_CLASS
		);

		if ( existing ) {
			return existing;
		}

		var body = document.createElement( 'div' );

		body.className = BODY_CLASS;

		/*
		 * toggle 다음의 모든 노드를 body 안으로 이동
		 *
		 * table뿐만 아니라 div, 텍스트 등도 지원
		 */
		var node = toggle.nextSibling;
		var nodes = [];

		while ( node ) {
			nodes.push( node );
			node = node.nextSibling;
		}

		nodes.forEach( function ( child ) {
			body.appendChild( child );
		} );

		target.appendChild( body );

		return body;
	}


	/**
	 * 초기 상태 확인
	 */
	function getInitialOpenState( target, toggle, body ) {
		/*
		 * MediaWiki가 mw-collapsed를 붙였다면 접힌 상태
		 */
		if ( target.classList.contains( 'mw-collapsed' ) ) {
			return false;
		}

		/*
		 * body가 display:none이면 접힌 상태
		 */
		if (
			window.getComputedStyle( body ).display === 'none'
		) {
			return false;
		}

		/*
		 * aria-expanded가 명시적으로 false
		 */
		if (
			toggle.getAttribute( 'aria-expanded' ) ===
			'false'
		) {
			return false;
		}

		return true;
	}


	/**
	 * 펼치기
	 */
	function openTarget( target, toggle, body ) {
		if (
			target.classList.contains(
				ANIMATING_CLASS
			)
		) {
			return;
		}

		target.classList.add(
			ANIMATING_CLASS
		);

		target.classList.remove(
			'mw-collapsed'
		);

		body.style.display = 'block';
		body.style.overflow = 'hidden';

		/*
		 * 실제 최종 높이 계산
		 */
		body.style.height = 'auto';

		var endHeight = body.scrollHeight;

		/*
		 * 0부터 시작
		 */
		body.style.height = '0px';

		/*
		 * 강제 reflow
		 */
		body.offsetHeight;

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

		animation.onfinish = function () {
			target.classList.remove(
				ANIMATING_CLASS
			);

			target.classList.add(
				OPEN_CLASS
			);

			body.style.height = '';
			body.style.overflow = '';
			body.style.display = '';

			toggle.setAttribute(
				'aria-expanded',
				'true'
			);

			animation.cancel();
		};
	}


	/**
	 * 접기
	 */
	function closeTarget( target, toggle, body ) {
		if (
			target.classList.contains(
				ANIMATING_CLASS
			)
		) {
			return;
		}

		target.classList.add(
			ANIMATING_CLASS
		);

		var startHeight =
			body.getBoundingClientRect().height;

		/*
		 * 이미 접혀있는 경우
		 */
		if ( startHeight <= 0 ) {
			target.classList.remove(
				ANIMATING_CLASS
			);

			return;
		}

		body.style.height =
			startHeight + 'px';

		body.style.overflow =
			'hidden';

		/*
		 * 강제 reflow
		 */
		body.offsetHeight;

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

		animation.onfinish = function () {
			target.classList.remove(
				ANIMATING_CLASS
			);

			target.classList.remove(
				OPEN_CLASS
			);

			target.classList.add(
				'mw-collapsed'
			);

			body.style.display = 'none';
			body.style.height = '';
			body.style.overflow = '';

			toggle.setAttribute(
				'aria-expanded',
				'false'
			);

			animation.cancel();
		};
	}


	/**
	 * 실제 클릭 처리
	 */
	function toggleTarget( toggle ) {
		var target = getTarget( toggle );

		if ( !target ) {
			return false;
		}

		var body = target.querySelector(
			':scope > .' + BODY_CLASS
		);

		if ( !body ) {
			body = createAnimationBody(
				target,
				toggle
			);
		}

		if ( !body ) {
			return false;
		}

		var isOpen =
			toggle.getAttribute(
				'aria-expanded'
			) === 'true' &&
			!target.classList.contains(
				'mw-collapsed'
			);

		if ( isOpen ) {
			closeTarget(
				target,
				toggle,
				body
			);
		} else {
			openTarget(
				target,
				toggle,
				body
			);
		}

		return true;
	}


	/**
	 * 각 custom collapsible 초기화
	 */
	function initializeTarget( toggle ) {
		var target = getTarget( toggle );

		if ( !target ) {
			return;
		}

		if (
			target.classList.contains(
				READY_CLASS
			)
		) {
			return;
		}

		var body = createAnimationBody(
			target,
			toggle
		);

		if ( !body ) {
			return;
		}

		target.classList.add(
			READY_CLASS
		);

		var isOpen = getInitialOpenState(
			target,
			toggle,
			body
		);

		if ( isOpen ) {
			target.classList.add(
				OPEN_CLASS
			);

			target.classList.remove(
				'mw-collapsed'
			);

			body.style.display = '';

			toggle.setAttribute(
				'aria-expanded',
				'true'
			);
		} else {
			target.classList.remove(
				OPEN_CLASS
			);

			target.classList.add(
				'mw-collapsed'
			);

			body.style.display = 'none';

			toggle.setAttribute(
				'aria-expanded',
				'false'
			);
		}
	}


	/**
	 * 페이지 전체 초기화
	 */
	function initializeAll() {
		var toggles = document.querySelectorAll(
			'.mw-customtoggle'
		);

		toggles.forEach(
			function ( toggle ) {
				if ( getToggleName( toggle ) ) {
					initializeTarget(
						toggle
					);
				}
			}
		);
	}


	/**
	 * 클릭
	 *
	 * MediaWiki 기본 토글보다 먼저 실행해서
	 * 기본 display:none 처리 방지.
	 */
	document.addEventListener(
		'click',
		function ( event ) {
			var toggle =
				event.target.closest(
					'.mw-customtoggle'
				);

			if ( !toggle ) {
				return;
			}

			if (
				!getToggleName( toggle )
			) {
				return;
			}

			event.preventDefault();
			event.stopPropagation();
			event.stopImmediatePropagation();

			toggleTarget(
				toggle
			);
		},
		true
	);


	/**
	 * Enter / Space
	 */
	document.addEventListener(
		'keydown',
		function ( event ) {
			if (
				event.key !== 'Enter' &&
				event.key !== ' '
			) {
				return;
			}

			var toggle =
				event.target.closest(
					'.mw-customtoggle'
				);

			if ( !toggle ) {
				return;
			}

			if (
				!getToggleName( toggle )
			) {
				return;
			}

			event.preventDefault();
			event.stopPropagation();
			event.stopImmediatePropagation();

			toggleTarget(
				toggle
			);
		},
		true
	);


	/**
	 * 초기 실행
	 */
	if (
		document.readyState ===
		'loading'
	) {
		document.addEventListener(
			'DOMContentLoaded',
			initializeAll
		);
	} else {
		initializeAll();
	}


	/**
	 * MediaWiki AJAX/동적 콘텐츠 대응
	 */
	if (
		window.mw &&
		mw.hook
	) {
		mw.hook(
			'wikipage.content'
		).add(
			function () {
				initializeAll();
			}
		);
	}

}() );