미디어위키:Common.js: 두 판 사이의 차이

편집 요약 없음
태그: 되돌려진 기여
편집 요약 없음
태그: 되돌려진 기여
1번째 줄: 1번째 줄:
/**
/**
  * MediaWiki 펼치기/접기 애니메이션
  * MediaWiki 커스텀 펼치기/접기 애니메이션
  *
  *
  * 기존 MediaWiki 토글 동작은 건드리지 않고,
  * 틀 구조를 수정하지 않고 작동합니다.
  * 실제로 표시/숨김되는 요소에 애니메이션만 추가합니다.
*
* 구조:
*
* <div id="mw-customcollapsible-XXX" class="mw-collapsible">
*
*    <div class="mw-customtoggle-XXX mw-customtoggle">
*        [ 펼치기 / 접기 ]
  *     </div>
*
*    실제 내용...
*
* </div>
*
* 토글 버튼은 항상 남겨두고
* 토글 버튼 뒤의 내용만 애니메이션합니다.
  */
  */


11번째 줄: 25번째 줄:
var DURATION = 300;
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 ) {
function getToggleName( toggle ) {
var prefix = 'mw-customtoggle-';
var prefix = 'mw-customtoggle-';
18번째 줄: 45번째 줄:
for ( i = 0; i < classes.length; i++ ) {
for ( i = 0; i < classes.length; i++ ) {
if ( classes[ i ].indexOf( prefix ) === 0 ) {
if ( classes[ i ].indexOf( prefix ) === 0 ) {
return classes[ i ].substring( prefix.length );
return classes[ i ].substring(
prefix.length
);
}
}
}
}
25번째 줄: 54번째 줄:
}
}


function getTarget( name ) {
 
/**
* 토글과 연결된 부모 찾기
*/
function getTarget( toggle ) {
var name = getToggleName( toggle );
 
if ( !name ) {
if ( !name ) {
return null;
return null;
}
}


/*
* 현재 구조에서는 toggle 자체가
* collapsible 안에 있으므로 우선 closest 사용
*/
var target = toggle.closest(
'.mw-collapsible'
);
if (
target &&
target.id ===
'mw-customcollapsible-' + name
) {
return target;
}
/*
* 혹시 구조가 다를 경우 ID로 검색
*/
return document.getElementById(
return document.getElementById(
'mw-customcollapsible-' + name
'mw-customcollapsible-' + name
35번째 줄: 89번째 줄:
}
}


/*
 
* target 자체 안에 toggle만 있는 경우,
/**
* target은 애니메이션 대상으로 사용하지 않습니다.
* 토글 버튼 이후의 모든 내용을
* 애니메이션용 DIV로 감싼다.
*
*
* 실제로 MediaWiki가 숨기고 표시하는 요소를
* 틀 원본 HTML은 수정할 필요 없음.
* 주변 DOM에서 찾습니다.
*/
*/
function findAnimatedContent( target, toggle ) {
function createAnimationBody( target, toggle ) {
var content = target.querySelector(
var existing = target.querySelector(
'.mw-collapsible-content'
':scope > .' + BODY_CLASS
);
);


if ( content && !content.contains( toggle ) ) {
if ( existing ) {
return content;
return existing;
}
}
var body = document.createElement( 'div' );
body.className = BODY_CLASS;


/*
/*
* 다음 형제 요소가 실제 내용인 구조 대응
* toggle 다음의 모든 노드를 body 안으로 이동
*
* table뿐만 아니라 div, 텍스트 등도 지원
*/
*/
var next = target.nextElementSibling;
var node = toggle.nextSibling;
var nodes = [];


if ( next && !next.classList.contains( 'mw-customtoggle' ) ) {
while ( node ) {
return next;
nodes.push( node );
node = node.nextSibling;
}
}


nodes.forEach( function ( child ) {
body.appendChild( child );
} );
target.appendChild( body );
return body;
}
/**
* 초기 상태 확인
*/
function getInitialOpenState( target, toggle, body ) {
/*
/*
* 부모 내부에서 target 다음 요소 탐색
* MediaWiki가 mw-collapsed를 붙였다면 접힌 상태
*/
*/
if ( target.parentElement ) {
if ( target.classList.contains( 'mw-collapsed' ) ) {
var siblings =
return false;
Array.prototype.slice.call(
}
target.parentElement.children
);


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


if (
/*
index !== -1 &&
* aria-expanded가 명시적으로 false
index + 1 < siblings.length
*/
) {
if (
return siblings[ index + 1 ];
toggle.getAttribute( 'aria-expanded' ) ===
}
'false'
) {
return false;
}
}


return null;
return true;
}
}


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


element.style.overflow = 'hidden';
target.classList.add(
element.style.height = 'auto';
ANIMATING_CLASS
);


var height = element.scrollHeight;
target.classList.remove(
'mw-collapsed'
);


element.style.height = '0px';
body.style.display = 'block';
body.style.overflow = 'hidden';


element.offsetHeight;
/*
* 실제 최종 높이 계산
*/
body.style.height = 'auto';
 
var endHeight = body.scrollHeight;
 
/*
* 0부터 시작
*/
body.style.height = '0px';
 
/*
* 강제 reflow
*/
body.offsetHeight;


var animation = element.animate(
var animation = body.animate(
[
[
{ height: '0px' },
{
{ height: height + 'px' }
height: '0px'
},
{
height: endHeight + 'px'
}
],
],
{
{
duration: DURATION,
duration: DURATION,
easing: 'ease',
easing: 'ease-out',
fill: 'forwards'
fill: 'forwards'
}
}
109번째 줄: 223번째 줄:


animation.onfinish = function () {
animation.onfinish = function () {
element.style.height = '';
target.classList.remove(
element.style.overflow = '';
ANIMATING_CLASS
);
 
target.classList.add(
OPEN_CLASS
);
 
body.style.height = '';
body.style.overflow = '';
body.style.display = '';
 
toggle.setAttribute(
'aria-expanded',
'true'
);


animation.cancel();
animation.cancel();
116번째 줄: 244번째 줄:
}
}


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


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


if ( height <= 0 ) {
return;
return;
}
}


element.style.overflow = 'hidden';
body.style.height =
element.style.height = height + 'px';
startHeight + 'px';
 
body.style.overflow =
'hidden';
 
/*
* 강제 reflow
*/
body.offsetHeight;


var animation = element.animate(
var animation = body.animate(
[
[
{ height: height + 'px' },
{
{ height: '0px' }
height:
startHeight + 'px'
},
{
height: '0px'
}
],
],
{
{
duration: DURATION,
duration: DURATION,
easing: 'ease',
easing: 'ease-in',
fill: 'forwards'
fill: 'forwards'
}
}
144번째 줄: 304번째 줄:


animation.onfinish = function () {
animation.onfinish = function () {
element.style.height = '';
target.classList.remove(
element.style.overflow = '';
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();
animation.cancel();
151번째 줄: 329번째 줄:
}
}


/**
* 실제 클릭 처리
*/
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(
document.addEventListener(
'click',
'click',
163번째 줄: 490번째 줄:
}
}


var name = getToggleName( toggle );
if (
 
!getToggleName( toggle )
if ( !name ) {
) {
return;
return;
}
}


var target = getTarget( name );
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
 
toggleTarget(
toggle
);
},
true
);


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


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


if ( !content ) {
if ( !toggle ) {
/*
* 내용을 특정할 수 없으면
* MediaWiki 기본 동작만 실행.
*
* 버튼 자체를 숨기는 일은 하지 않음.
*/
return;
return;
}
}


/*
if (
* 여기서는 기본 이벤트를 차단하지 않습니다.
!getToggleName( toggle )
* MediaWiki가 원래대로 펼치고 접게 둡니다.
) {
*/
return;
}


var wasHidden =
event.preventDefault();
window.getComputedStyle( content )
event.stopPropagation();
.display === 'none' ||
event.stopImmediatePropagation();
content.getBoundingClientRect()
.height === 0;


/*
toggleTarget(
* MediaWiki 기본 토글 처리 직후 상태를 읽기 위해
toggle
* 다음 task에서 실행
*/
window.setTimeout(
function () {
var isHidden =
window.getComputedStyle(
content
).display === 'none' ||
content.getBoundingClientRect()
.height === 0;
 
if ( wasHidden && !isHidden ) {
animateOpen( content );
} else if (
!wasHidden &&
isHidden
) {
/*
* 이미 display:none이 되어버린 경우에는
* 애니메이션할 높이가 없기 때문에
* 이 구조에서는 접기 애니메이션을
* 완벽히 재현할 수 없습니다.
*/
}
},
0
);
);
},
},
false
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();
}
);
}


}() );
}() );

2026년 8월 20일 (목) 13:23 판

/**
 * 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();
			}
		);
	}

}() );