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

편집 요약 없음
태그: 되돌려진 기여
편집 요약 없음
태그: 수동 되돌리기
2번째 줄: 2번째 줄:
  * MediaWiki 커스텀 펼치기/접기 애니메이션
  * MediaWiki 커스텀 펼치기/접기 애니메이션
  *
  *
  * 틀 구조 수정 불필요.
  * 지원 예:
  *
  *
  * 지원 구조:
  * .mw-customtoggle-KSC_LEADER
*          ↕
* .mw-customcollapsible-KSC_LEADER
  *
  *
  * <div id="mw-customcollapsible-XXX" class="mw-collapsible">
  * 식별자 이름은 자유롭게 사용 가능.
*    <div class="mw-customtoggle-XXX mw-customtoggle">
*        [ 펼치기 · 접기 ]
*    </div>
*
*    <table>...</table>
* </div>
*
* 토글 버튼은 항상 남겨두고
* 버튼 뒤의 내용만 슬라이드합니다.
  */
  */


22번째 줄: 15번째 줄:


var DURATION = 300;
var DURATION = 300;
var ANIMATING_CLASS = 'mw-custom-collapse-animating';


/**
* 토글 요소에서 식별자 추출
*
* 예:
* mw-customtoggle-KSC_LEADER
* → KSC_LEADER
*/
function getToggleName( toggle ) {
function getToggleName( toggle ) {
var prefix = 'mw-customtoggle-';
var prefix = 'mw-customtoggle-';
37번째 줄: 38번째 줄:
}
}


function getTarget( toggle ) {
/**
var name = getToggleName( toggle );
* CSS selector escape
*/
function escapeSelector( value ) {
if ( window.CSS && CSS.escape ) {
return CSS.escape( value );
}
 
return value.replace(
/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g,
'\\\\$1'
);
}


/**
* 식별자에 대응하는 collapsible 대상 찾기
*/
function getTarget( name ) {
if ( !name ) {
if ( !name ) {
return null;
return null;
}
}


return document.getElementById(
var target = document.querySelector(
'.mw-customcollapsible-' +
escapeSelector( name )
);
 
if ( target ) {
return target;
}
 
target = document.getElementById(
'mw-customcollapsible-' + name
'mw-customcollapsible-' + name
);
);
return target;
}
}


/*
/**
* 토글 버튼 다음에 있는 실제 내용들을 찾음.
* 실제 애니메이션을 적용할 콘텐츠 찾기
*
* 여성 아나운서처럼 같은 div 안에 table이 있는 경우 대응.
*/
*/
function getInternalContents( target, toggle ) {
function getContent( target ) {
var contents = [];
var content = target.querySelector(
var node = toggle.nextElementSibling;
'.mw-collapsible-content'
);


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


return contents;
return target;
}
}


/*
/**
* 남성 아나운서처럼 target 바깥 바로 다음에
* 요소 원래 display 타입 보존
* table이 있는 경우를 위한 처리.
*
* table 요소에 display:block을 강제하면
* 레이아웃이 깨질 수 있으므로 태그별 기본값 사용.
*/
*/
function getExternalContents( target ) {
function getNaturalDisplay( element ) {
var contents = [];
switch ( element.tagName ) {
var node = target.nextElementSibling;
case 'TABLE':
return 'table';


while ( node ) {
case 'TBODY':
/*
return 'table-row-group';
* 다음 펼치기/접기 영역이 시작되면 종료
 
*/
case 'THEAD':
if (
return 'table-header-group';
node.id &&
 
node.id.indexOf( 'mw-customcollapsible-' ) === 0
case 'TFOOT':
) {
return 'table-footer-group';
break;
}


contents.push( node );
case 'TR':
return 'table-row';


/*
case 'TD':
* 현재 구조에서는 표 하나면 충분하므로
case 'TH':
* 첫 실제 요소만 대상으로 함
return 'table-cell';
*/
break;
}


return contents;
case 'COLGROUP':
}
return 'table-column-group';


function getContents( target, toggle ) {
case 'COL':
var internal = getInternalContents(
return 'table-column';
target,
toggle
);


if ( internal.length ) {
case 'CAPTION':
return internal;
return 'table-caption';
}


return getExternalContents( target );
case 'LI':
}
return 'list-item';


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


if ( !contents.length ) {
/**
return false;
* 현재 접혀있는지 확인
*/
function isCollapsed( target, content ) {
if ( target.classList.contains( 'mw-collapsed' ) ) {
return true;
}
}


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


/*
/**
* 펼치기
* 펼치기
*/
*/
function openContents( toggle, contents ) {
function expand( target, content, toggle ) {
toggle.setAttribute(
if ( content.classList.contains( ANIMATING_CLASS ) ) {
'aria-expanded',
return;
'true'
}
);
 
content.classList.add( ANIMATING_CLASS );


contents.forEach( function ( element ) {
target.classList.remove( 'mw-collapsed' );
if ( element._collapseAnimation ) {
element._collapseAnimation.cancel();
}


/*
/*
* 먼저 원래 display 복구
* display:none 상태에서는 높이를 구할 수 없으므로
*/
* 원래 요소 유형에 맞는 display로 임시 복원
element.style.display = '';
*/
content.style.display = getNaturalDisplay( content );
content.style.height = 'auto';
content.style.overflow = 'hidden';


/*
var targetHeight = content.scrollHeight;
* 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;
/*
* 0 높이에서 시작
*/
content.style.height = '0px';


element.style.overflow = 'hidden';
/*
* 강제 reflow
*/
content.offsetHeight;


var animation = element.animate(
var animation = content.animate(
[
[
{
height: '0px'
},
{
height: endHeight + 'px'
}
],
{
{
duration: DURATION,
height: '0px'
easing: 'ease',
},
fill: 'forwards'
{
height: targetHeight + 'px'
}
}
);
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
 
animation.onfinish = function () {
content.classList.remove( ANIMATING_CLASS );


element._collapseAnimation =
/*
animation;
* 원래 CSS가 다시 레이아웃을 결정하도록
* 인라인 스타일 제거
*/
content.style.display = '';
content.style.height = '';
content.style.overflow = '';


animation.onfinish = function () {
toggle.setAttribute(
element.style.height = '';
'aria-expanded',
element.style.overflow = '';
'true'
element.style.display = '';
);


element._collapseAnimation =
animation.cancel();
null;
};


animation.cancel();
animation.oncancel = function () {
};
content.classList.remove( ANIMATING_CLASS );
} );
};
}
}


/*
/**
* 접기
* 접기
*/
*/
function closeContents( toggle, contents ) {
function collapse( target, content, toggle ) {
toggle.setAttribute(
if ( content.classList.contains( ANIMATING_CLASS ) ) {
'aria-expanded',
return;
'false'
}
);


contents.forEach( function ( element ) {
content.classList.add( ANIMATING_CLASS );
if ( element._collapseAnimation ) {
element._collapseAnimation.cancel();
}


var startHeight =
/*
element.getBoundingClientRect().height;
* 현재 높이 측정
*/
var startHeight =
content.getBoundingClientRect().height;


if ( startHeight <= 0 ) {
/*
element.style.display = 'none';
* 애니메이션 동안 display 타입 유지
return;
*/
}
content.style.display = getNaturalDisplay( content );
content.style.height = startHeight + 'px';
content.style.overflow = 'hidden';


element.style.overflow = 'hidden';
/*
* 강제 reflow
*/
content.offsetHeight;


var animation = element.animate(
var animation = content.animate(
[
[
{
height:
startHeight + 'px'
},
{
height: '0px'
}
],
{
{
duration: DURATION,
height: startHeight + 'px'
easing: 'ease',
},
fill: 'forwards'
{
height: '0px'
}
}
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
animation.onfinish = function () {
target.classList.add( 'mw-collapsed' );
content.classList.remove( ANIMATING_CLASS );
/*
* 접힌 상태 유지
*/
content.style.display = 'none';
/*
* height와 overflow만 제거
*/
content.style.height = '';
content.style.overflow = '';
toggle.setAttribute(
'aria-expanded',
'false'
);
);


element._collapseAnimation =
animation.cancel();
animation;
};


animation.onfinish = function () {
animation.oncancel = function () {
element.style.display = 'none';
content.classList.remove( ANIMATING_CLASS );
element.style.height = '';
};
element.style.overflow = '';
}


element._collapseAnimation =
/**
null;
* 실제 토글 처리
*/
function toggleCollapsible( toggle ) {
var name = getToggleName( toggle );


animation.cancel();
if ( !name ) {
};
return false;
} );
}
}


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


if ( !target ) {
if ( !target ) {
return;
return false;
}
}


var contents = getContents(
var content = getContent( target );
target,
toggle
);


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


/*
if ( isCollapsed( target, content ) ) {
* 이제 대상이 확실하게 있을 때만
expand(
* MediaWiki 기본 즉시 접기/펼치기를 막음.
target,
*/
content,
event.preventDefault();
toggle
event.stopPropagation();
event.stopImmediatePropagation();
 
if ( isOpen( toggle, contents ) ) {
closeContents(
toggle,
contents
);
);
} else {
} else {
openContents(
collapse(
toggle,
target,
contents
content,
toggle
);
);
}
}
}


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


function initialize() {
/**
var toggles =
* 마우스 클릭
document.querySelectorAll(
*
* MediaWiki 기본 customtoggle보다 먼저 실행하기 위해
* capture=true 사용
*/
document.addEventListener(
'click',
function ( event ) {
var toggle = event.target.closest(
'.mw-customtoggle'
'.mw-customtoggle'
);
);


toggles.forEach( function ( toggle ) {
if ( !toggle ) {
return;
}
 
if ( !getToggleName( toggle ) ) {
return;
}
 
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
 
toggleCollapsible( toggle );
},
true
);
 
/**
* Enter / Space 키 지원
*/
document.addEventListener(
'keydown',
function ( event ) {
if (
if (
!getToggleName( toggle ) ||
event.key !== 'Enter' &&
toggle.dataset.collapseAnimationReady === '1'
event.key !== ' '
) {
) {
return;
return;
}
}


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


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


toggle.addEventListener(
if ( !getToggleName( toggle ) ) {
'keydown',
return;
handleKeydown
}
);
} );
}


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


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


}() );
}() );

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

/**
 * MediaWiki 커스텀 펼치기/접기 애니메이션
 *
 * 지원 예:
 *
 * .mw-customtoggle-KSC_LEADER
 *          ↕
 * .mw-customcollapsible-KSC_LEADER
 *
 * 식별자 이름은 자유롭게 사용 가능.
 */

( function () {
	'use strict';

	var DURATION = 300;
	var ANIMATING_CLASS = 'mw-custom-collapse-animating';

	/**
	 * 토글 요소에서 식별자 추출
	 *
	 * 예:
	 * mw-customtoggle-KSC_LEADER
	 * → KSC_LEADER
	 */
	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;
	}

	/**
	 * CSS selector escape
	 */
	function escapeSelector( value ) {
		if ( window.CSS && CSS.escape ) {
			return CSS.escape( value );
		}

		return value.replace(
			/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g,
			'\\\\$1'
		);
	}

	/**
	 * 식별자에 대응하는 collapsible 대상 찾기
	 */
	function getTarget( name ) {
		if ( !name ) {
			return null;
		}

		var target = document.querySelector(
			'.mw-customcollapsible-' +
			escapeSelector( name )
		);

		if ( target ) {
			return target;
		}

		target = document.getElementById(
			'mw-customcollapsible-' + name
		);

		return target;
	}

	/**
	 * 실제 애니메이션을 적용할 콘텐츠 찾기
	 */
	function getContent( target ) {
		var content = target.querySelector(
			'.mw-collapsible-content'
		);

		if ( content ) {
			return content;
		}

		return target;
	}

	/**
	 * 요소 원래 display 타입 보존
	 *
	 * table 요소에 display:block을 강제하면
	 * 레이아웃이 깨질 수 있으므로 태그별 기본값 사용.
	 */
	function getNaturalDisplay( element ) {
		switch ( element.tagName ) {
			case 'TABLE':
				return 'table';

			case 'TBODY':
				return 'table-row-group';

			case 'THEAD':
				return 'table-header-group';

			case 'TFOOT':
				return 'table-footer-group';

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

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

			case 'COLGROUP':
				return 'table-column-group';

			case 'COL':
				return 'table-column';

			case 'CAPTION':
				return 'table-caption';

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

			default:
				return 'block';
		}
	}

	/**
	 * 현재 접혀있는지 확인
	 */
	function isCollapsed( target, content ) {
		if ( target.classList.contains( 'mw-collapsed' ) ) {
			return true;
		}

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

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

		content.classList.add( ANIMATING_CLASS );

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

		/*
		 * display:none 상태에서는 높이를 구할 수 없으므로
		 * 원래 요소 유형에 맞는 display로 임시 복원
		 */
		content.style.display = getNaturalDisplay( content );
		content.style.height = 'auto';
		content.style.overflow = 'hidden';

		var targetHeight = content.scrollHeight;

		/*
		 * 0 높이에서 시작
		 */
		content.style.height = '0px';

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

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

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

			/*
			 * 원래 CSS가 다시 레이아웃을 결정하도록
			 * 인라인 스타일 제거
			 */
			content.style.display = '';
			content.style.height = '';
			content.style.overflow = '';

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

			animation.cancel();
		};

		animation.oncancel = function () {
			content.classList.remove( ANIMATING_CLASS );
		};
	}

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

		content.classList.add( ANIMATING_CLASS );

		/*
		 * 현재 높이 측정
		 */
		var startHeight =
			content.getBoundingClientRect().height;

		/*
		 * 애니메이션 동안 display 타입 유지
		 */
		content.style.display = getNaturalDisplay( content );
		content.style.height = startHeight + 'px';
		content.style.overflow = 'hidden';

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

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

		animation.onfinish = function () {
			target.classList.add( 'mw-collapsed' );

			content.classList.remove( ANIMATING_CLASS );

			/*
			 * 접힌 상태 유지
			 */
			content.style.display = 'none';

			/*
			 * height와 overflow만 제거
			 */
			content.style.height = '';
			content.style.overflow = '';

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

			animation.cancel();
		};

		animation.oncancel = function () {
			content.classList.remove( ANIMATING_CLASS );
		};
	}

	/**
	 * 실제 토글 처리
	 */
	function toggleCollapsible( toggle ) {
		var name = getToggleName( toggle );

		if ( !name ) {
			return false;
		}

		var target = getTarget( name );

		if ( !target ) {
			return false;
		}

		var content = getContent( target );

		if ( !content ) {
			return false;
		}

		if ( isCollapsed( target, content ) ) {
			expand(
				target,
				content,
				toggle
			);
		} else {
			collapse(
				target,
				content,
				toggle
			);
		}

		return true;
	}

	/**
	 * 마우스 클릭
	 *
	 * MediaWiki 기본 customtoggle보다 먼저 실행하기 위해
	 * capture=true 사용
	 */
	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();

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

			toggleCollapsible( toggle );
		},
		true
	);

}() );