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

편집 요약 없음
태그: 되돌려진 기여
편집 요약 없음
태그: 되돌려진 기여
1번째 줄: 1번째 줄:
/**
* MediaWiki custom toggle 애니메이션
*
* 예:
*
* mw-customtoggle-MALE
*      ↕
* mw-customcollapsible-MALE
*
* mw-customtoggle-FEMALE
*      ↕
* mw-customcollapsible-FEMALE
*
* 여러 개를 동시에 사용해도 각각 독립적으로 작동합니다.
*/
( function () {
( function () {
'use strict';
'use strict';


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


/**
document.addEventListener(
* mw-customtoggle-XXX 에서 XXX 추출
'click',
*/
function ( event ) {
function getToggleName( toggle ) {
var toggle = event.target.closest( '.mw-customtoggle' );
var prefix = 'mw-customtoggle-';
var classes = toggle.classList;
var i;


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


return null;
var name = null;
}


/**
toggle.classList.forEach( function ( className ) {
* CSS 선택자 escape
if ( className.indexOf( 'mw-customtoggle-' ) === 0 ) {
*/
name = className.substring(
function escapeSelector( value ) {
'mw-customtoggle-'.length
if ( window.CSS && CSS.escape ) {
);
return CSS.escape( value );
}
}
} );


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


/**
var target = document.getElementById(
* 해당 이름에 연결된 모든 대상 찾기
'mw-customcollapsible-' + name
*/
function getTargets( name ) {
if ( !name ) {
return [];
}
 
var selector =
'.mw-customcollapsible-' +
escapeSelector( name );
 
var targets =
Array.prototype.slice.call(
document.querySelectorAll( selector )
);
);


/*
if ( !target ) {
* id 형식도 지원
return;
*/
}
var idTarget = document.getElementById(
'mw-customcollapsible-' + name
);


if (
/*
idTarget &&
* 실제 내용 영역이 있으면 그것을 사용하고,
targets.indexOf( idTarget ) === -1
* 없으면 target 자체 사용.
) {
*
targets.push( idTarget );
* 이제 toggle은 target 밖에 있으므로
}
* 버튼이 사라지는 문제가 없음.
*/
var content =
target.querySelector( '.mw-collapsible-content' ) ||
target;


return targets;
var collapsed =
}
target.classList.contains( 'mw-collapsed' ) ||
window.getComputedStyle( content ).display === 'none';


/**
/*
* 실제 애니메이션 대상 결정
* MediaWiki 기본 토글 방지
*
*/
* 중요:
event.preventDefault();
* 토글 버튼 자체가 들어있는 요소 전체를
event.stopPropagation();
* display:none으로 만들지 않도록 한다.
event.stopImmediatePropagation();
*/
function getContent( target, toggle ) {
var content = target.querySelector(
'.mw-collapsible-content'
);


if ( content ) {
if ( collapsed ) {
return content;
expand( target, content );
}
} else {
collapse( target, content );
}
},
true
);


/*
* target 자체가 toggle을 포함하지 않는다면
* target 자체를 접어도 안전하다.
*/
if ( !target.contains( toggle ) ) {
return target;
}
/*
* toggle이 target 내부에 있다면
* target 전체를 숨기면 버튼까지 사라지므로
* 애니메이션하지 않는다.
*/
return null;
}
/**
* 태그의 자연스러운 display 값
*/
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 ) {
function expand( target, content ) {
if (
target.classList.remove( 'mw-collapsed' );
content.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
 
content.classList.add(
ANIMATING_CLASS
);
 
target.classList.remove(
'mw-collapsed'
);
 
content.style.display =
getNaturalDisplay( content );


content.style.display = 'block';
content.style.height = 'auto';
content.style.height = 'auto';
content.style.overflow = 'hidden';
content.style.overflow = 'hidden';


var targetHeight =
var height = content.scrollHeight;
content.scrollHeight;


content.style.height = '0px';
content.style.height = '0px';


/*
* reflow
*/
content.offsetHeight;
content.offsetHeight;


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


animation.onfinish = function () {
animation.onfinish = function () {
content.classList.remove(
ANIMATING_CLASS
);
/*
* 애니메이션 후 원래 CSS 상태로 복구
*/
content.style.display = '';
content.style.display = '';
content.style.height = '';
content.style.height = '';
243번째 줄: 100번째 줄:
}
}


/**
* 접기
*/
function collapse( target, content ) {
function collapse( target, content ) {
if (
var height =
content.classList.contains(
content.getBoundingClientRect().height;
ANIMATING_CLASS
 
)
content.style.height = height + 'px';
) {
content.style.overflow = 'hidden';
return;
}


content.classList.add(
var animation = content.animate(
ANIMATING_CLASS
[
{ height: height + 'px' },
{ height: '0px' }
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);
);
var startHeight =
content.getBoundingClientRect()
.height;
content.style.display =
getNaturalDisplay( content );
content.style.height =
startHeight + 'px';
content.style.overflow =
'hidden';
content.offsetHeight;
var animation =
content.animate(
[
{
height:
startHeight + 'px'
},
{
height: '0px'
}
],
{
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);


animation.onfinish = function () {
animation.onfinish = function () {
target.classList.add(
target.classList.add( 'mw-collapsed' );
'mw-collapsed'
);
 
content.classList.remove(
ANIMATING_CLASS
);
 
content.style.display =
'none';


content.style.display = 'none';
content.style.height = '';
content.style.height = '';
content.style.overflow = '';
content.style.overflow = '';
310번째 줄: 129번째 줄:
};
};
}
}
/**
* 실제 토글 실행
*/
function toggleCollapsible( toggle ) {
var name =
getToggleName( toggle );
if ( !name ) {
return false;
}
var targets =
getTargets( name );
if ( !targets.length ) {
return false;
}
var handled = false;
targets.forEach(
function ( target ) {
var content =
getContent(
target,
toggle
);
/*
* 버튼을 포함하고 있는 요소 자체라면
* 숨기지 않는다.
*/
if ( !content ) {
return;
}
if (
isCollapsed(
target,
content
)
) {
expand(
target,
content
);
} else {
collapse(
target,
content
);
}
handled = true;
}
);
/*
* 버튼 aria 상태도 변경
*/
if ( handled ) {
var expanded =
toggle.getAttribute(
'aria-expanded'
) === 'true';
toggle.setAttribute(
'aria-expanded',
expanded
? 'false'
: 'true'
);
}
return handled;
}
/**
* 클릭 처리
*/
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
);


}() );
}() );

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

( function () {
	'use strict';

	var DURATION = 300;

	document.addEventListener(
		'click',
		function ( event ) {
			var toggle = event.target.closest( '.mw-customtoggle' );

			if ( !toggle ) {
				return;
			}

			var name = null;

			toggle.classList.forEach( function ( className ) {
				if ( className.indexOf( 'mw-customtoggle-' ) === 0 ) {
					name = className.substring(
						'mw-customtoggle-'.length
					);
				}
			} );

			if ( !name ) {
				return;
			}

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

			if ( !target ) {
				return;
			}

			/*
			 * 실제 내용 영역이 있으면 그것을 사용하고,
			 * 없으면 target 자체 사용.
			 *
			 * 이제 toggle은 target 밖에 있으므로
			 * 버튼이 사라지는 문제가 없음.
			 */
			var content =
				target.querySelector( '.mw-collapsible-content' ) ||
				target;

			var collapsed =
				target.classList.contains( 'mw-collapsed' ) ||
				window.getComputedStyle( content ).display === 'none';

			/*
			 * MediaWiki 기본 토글 방지
			 */
			event.preventDefault();
			event.stopPropagation();
			event.stopImmediatePropagation();

			if ( collapsed ) {
				expand( target, content );
			} else {
				collapse( target, content );
			}
		},
		true
	);

	function expand( target, content ) {
		target.classList.remove( 'mw-collapsed' );

		content.style.display = 'block';
		content.style.height = 'auto';
		content.style.overflow = 'hidden';

		var height = content.scrollHeight;

		content.style.height = '0px';

		content.offsetHeight;

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

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

			animation.cancel();
		};
	}

	function collapse( target, content ) {
		var height =
			content.getBoundingClientRect().height;

		content.style.height = height + 'px';
		content.style.overflow = 'hidden';

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

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

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

			animation.cancel();
		};
	}

}() );