미디어위키:Common.js

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

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

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
/**
 * MediaWiki 커스텀 펼치기 / 접기 애니메이션
 *
 * 지원:
 *
 * 1. 일반 MediaWiki 구조
 *
 * .mw-customtoggle-XXX
 * #mw-customcollapsible-XXX
 * └─ .mw-collapsible-content
 *
 *
 * 2. 동일 ID를 2개 사용하는 전환형 둘러보기 틀
 *
 * #mw-customcollapsible-XXX   ← 처음 보이는 버튼
 * └─ .mw-customtoggle-XXX
 *
 * #mw-customcollapsible-XXX   ← 처음 숨겨진 실제 내용
 * ├─ .mw-customtoggle-XXX
 * └─ table / .sakura-table-wrapper
 *
 *
 * 클릭 시 같은 XXX를 가진 모든 collapsible을
 * 각각 현재 상태의 반대로 전환합니다.
 */

( function ( $, mw ) {
	'use strict';

	var DURATION = 300;
	var EASING = 'swing';

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


	/**
	 * attribute selector escape
	 */
	function escapeAttribute( value ) {
		return String( value )
			.replace( /\\/g, '\\\\' )
			.replace( /"/g, '\\"' );
	}


	/**
	 * 동일한 이름을 사용하는
	 * 모든 collapsible 대상 가져오기
	 */
	function getTargets( name ) {
		var id =
			'mw-customcollapsible-' + name;

		return document.querySelectorAll(
			'[id="' +
			escapeAttribute( id ) +
			'"]'
		);
	}


	/**
	 * 직계 .mw-collapsible-content 찾기
	 *
	 * 중첩 collapsible까지 잘못 선택하지 않도록
	 * children만 검사합니다.
	 */
	function getDirectContent( target ) {
		var children = target.children;
		var i;

		for ( i = 0; i < children.length; i++ ) {
			if (
				children[ i ].classList.contains(
					'mw-collapsible-content'
				)
			) {
				return children[ i ];
			}
		}

		return null;
	}


	/**
	 * 실제 애니메이션 대상
	 *
	 * MediaWiki 표준:
	 * .mw-collapsible-content
	 *
	 * 현재 LACTEA 구조:
	 * target 자체
	 */
	function getAnimatedElement( target ) {
		var content =
			getDirectContent( target );

		if ( content ) {
			return content;
		}

		return target;
	}


	/**
	 * 현재 접혀 있는지 확인
	 */
	function isCollapsed( target, element ) {

		if (
			target.classList.contains(
				'mw-collapsed'
			)
		) {
			return true;
		}

		return (
			window.getComputedStyle(
				element
			).display === 'none'
		);
	}


	/**
	 * 펼칠 때 사용할 display 계산
	 */
	function getOpenDisplay( element ) {

		/*
		 * 이전에 저장된 display가 있으면 사용
		 */
		var stored =
			$( element ).data(
				'mwAnimatedDisplay'
			);

		if ( stored ) {
			return stored;
		}


		var display =
			window.getComputedStyle(
				element
			).display;


		/*
		 * 현재 보이는 상태라면 저장
		 */
		if (
			display &&
			display !== 'none'
		) {

			$( element ).data(
				'mwAnimatedDisplay',
				display
			);

			return display;
		}


		/*
		 * mw-collapsible 자체를 펼치는 구조
		 */
		if (
			element.classList.contains(
				'mw-collapsible'
			)
		) {

			/*
			 * width가 100%인 내용 영역은 block이
			 * 가장 자연스럽습니다.
			 */
			if (
				element.style.width === '100%'
			) {
				return 'block';
			}

			return 'inline-block';
		}


		switch ( element.tagName ) {

			case 'TABLE':
				return 'table';

			case 'TR':
				return 'table-row';

			case 'TD':
			case 'TH':
				return 'table-cell';

			case 'LI':
				return 'list-item';

			default:
				return 'block';
		}
	}


	/**
	 * 현재 display 값을 기억
	 *
	 * 닫기 전에 호출해야 함
	 */
	function rememberDisplay( element ) {

		var display =
			window.getComputedStyle(
				element
			).display;

		if (
			display &&
			display !== 'none'
		) {
			$( element ).data(
				'mwAnimatedDisplay',
				display
			);
		}
	}


	/**
	 * 같은 name의 모든 토글에
	 * aria-expanded 설정
	 */
	function updateToggleAria(
		name,
		expanded
	) {

		var className =
			'mw-customtoggle-' + name;

		var toggles =
			document.getElementsByClassName(
				className
			);

		var i;

		for ( i = 0; i < toggles.length; i++ ) {
			toggles[ i ].setAttribute(
				'aria-expanded',
				expanded ?
					'true' :
					'false'
			);
		}
	}


	/**
	 * 펼치기
	 */
	function expand(
		target,
		element
	) {

		var $element =
			$( element );


		$element.stop(
			true,
			false
		);


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


		/*
		 * 표준 MediaWiki 방식
		 *
		 * target 안의
		 * .mw-collapsible-content만 처리
		 */
		if ( element !== target ) {

			$element
				.stop( true, false )
				.slideDown(
					DURATION
				);

			return;
		}


		/*
		 * LACTEA 같은 구조
		 *
		 * mw-collapsible 자체가 display:none이므로
		 * 직접 높이를 애니메이션
		 */

		var display =
			getOpenDisplay(
				element
			);


		$element.css(
			{
				display: display,
				height: 'auto',
				overflow: 'hidden'
			}
		);


		var targetHeight =
			element.scrollHeight;


		$element.css(
			{
				height: 0,
				opacity: 0
			}
		);


		/*
		 * reflow
		 */
		void element.offsetHeight;


		$element.animate(
			{
				height:
					targetHeight,

				opacity:
					1
			},
			{
				duration:
					DURATION,

				easing:
					EASING,

				complete:
					function () {

						$element.css(
							{
								height: '',
								overflow: '',
								opacity: ''
							}
						);
					}
			}
		);
	}


	/**
	 * 접기
	 */
	function collapse(
		target,
		element
	) {

		var $element =
			$( element );


		$element.stop(
			true,
			false
		);


		/*
		 * 현재 display를 반드시 먼저 기억
		 */
		rememberDisplay(
			element
		);


		/*
		 * 표준 MediaWiki 구조
		 */
		if ( element !== target ) {

			$element.slideUp(
				DURATION,
				function () {

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

			return;
		}


		/*
		 * mw-collapsible 자체가 접히는 구조
		 */

		var startHeight =
			element
				.getBoundingClientRect()
				.height;


		$element.css(
			{
				height:
					startHeight,

				overflow:
					'hidden',

				opacity:
					1
			}
		);


		/*
		 * reflow
		 */
		void element.offsetHeight;


		$element.animate(
			{
				height: 0,
				opacity: 0
			},
			{
				duration:
					DURATION,

				easing:
					EASING,

				complete:
					function () {

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


						$element.css(
							{
								display:
									'none',

								height:
									'',

								overflow:
									'',

								opacity:
									''
							}
						);
					}
			}
		);
	}


	/**
	 * 하나의 name에 연결된 대상 전체 토글
	 */
	function toggleGroup( toggle ) {

		var name =
			getToggleName(
				toggle
			);

		if ( !name ) {
			return false;
		}


		var targets =
			getTargets(
				name
			);

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


		var i;

		/*
		 * 핵심:
		 *
		 * 하나만 선택하지 않고
		 * 같은 이름을 가진 모든 target을
		 * 각자 반대 상태로 변경
		 */
		for (
			i = 0;
			i < targets.length;
			i++
		) {

			var target =
				targets[ i ];

			var element =
				getAnimatedElement(
					target
				);


			if (
				isCollapsed(
					target,
					element
				)
			) {

				expand(
					target,
					element
				);

			} else {

				collapse(
					target,
					element
				);
			}
		}


		/*
		 * 이 형태에서는 보이는 토글과 숨겨진 토글이
		 * 교대하므로 aria-expanded를 특정 하나에
		 * 일괄 적용하는 것은 생략합니다.
		 */


		return true;
	}


	/**
	 * 클릭
	 *
	 * MediaWiki 기본 customtoggle보다
	 * 먼저 실행
	 */
	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();


			toggleGroup(
				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();


			toggleGroup(
				toggle
			);
		},
		true
	);

}( jQuery, mediaWiki ) );