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

편집 요약 없음
편집 요약 없음
3번째 줄: 3번째 줄:
  *
  *
  * 지원:
  * 지원:
* - 일반 mw-collapsible-content
* - mw-collapsible 자체가 display:none 되는 구조
* - 동일 ID 2개 교대형 구조
* - flex 형태 복수 토글
* - 중첩 table / rowspan / infobox
  *
  *
  * 1. 일반 MediaWiki 구조
  * 특징:
*
  * - opacity 없음
  * .mw-customtoggle-XXX
  * - 내부 table 자체의 height는 변경하지 않음
  * #mw-customcollapsible-XXX
  * - wrapper의 height + clip-path만 애니메이션
* └─ .mw-collapsible-content
*
*
* 2. 동일 ID 2개를 사용하는 교대형 구조
*
* #mw-customcollapsible-XXX
* └─ [ 버튼 ]
*
* #mw-customcollapsible-XXX.mw-collapsed
* ├─ [ 버튼 ]
* └─ 실제 내용
*
*
* 3. 여러 독립 토글
*
* TWICE_KOR
* TWICE_OVS
* TWICE_ETC
* TWICE_REL
*
*
* 4. 정보상자 / table / rowspan 내부 지원
*
* mw-collapsible 자체의 height를 직접 변경하지 않고
* 내부 wrapper만 slide 처리하여
* table 레이아웃 재계산 문제를 줄임.
*
*
  * 애니메이션:
* height만 사용
* opacity 사용하지 않음
  */
  */


44번째 줄: 18번째 줄:
'use strict';
'use strict';


var DURATION = 200;
var DURATION = 180;
var EASING = 'swing';
 
var EASING =
'cubic-bezier(0.4, 0, 0.2, 1)';


var DATA_DISPLAY =
var DATA_DISPLAY =
'mwCustomCollapseDisplay';
'mwCustomCollapseDisplay';


var INNER_CLASS =
var DATA_ANIMATION =
'mw-custom-collapse-inner';
'mwCustomCollapseAnimation';
 
var DATA_WRAPPED =
'mwCustomCollapseWrapped';




/**
/**
* 토글 클래스에서 식별자 추출
* mw-customtoggle-XXX
*
* mw-customtoggle-Flora_LACTEA_01
* →
* →
* Flora_LACTEA_01
* XXX
*/
*/
function getToggleName( toggle ) {
function getToggleName( toggle ) {
110번째 줄: 81번째 줄:


/**
/**
* 같은 name을 가진 모든 target 검색
* 같은 이름의 모든 target
*/
*/
function getTargets( name ) {
function getTargets( name ) {
if ( !name ) {
return [];
}
var id =
var id =
'mw-customcollapsible-' +
'mw-customcollapsible-' +
130번째 줄: 97번째 줄:


/**
/**
* target의 직계
* 직계 mw-collapsible-content 검색
* .mw-collapsible-content 검색
*
* 중첩 collapsible 오선택 방지.
*/
*/
function getDirectContent( target ) {
function getDirectContent( target ) {
161번째 줄: 125번째 줄:


/**
/**
* 기존에 만든 내부 wrapper 검색
* 실제 애니메이션 요소
*/
function getDirectInnerWrapper( target ) {
var children =
target.children;
 
var i;
 
for (
i = 0;
i < children.length;
i++
) {
if (
children[ i ].classList &&
children[ i ].classList.contains(
INNER_CLASS
)
) {
return children[ i ];
}
}
 
return null;
}
 
 
/**
* target 내부에 애니메이션용 wrapper 생성
*
* .mw-collapsible-content가 없는 구조에서 사용.
*
* target 자체 height를 변경하지 않고
* 내부 wrapper만 slide 처리.
*/
function createInnerWrapper( target ) {
var existing =
getDirectInnerWrapper(
target
);
 
if ( existing ) {
return existing;
}
 
 
var wrapper =
document.createElement(
'div'
);
 
wrapper.className =
INNER_CLASS;
 
 
/*
* 기존 자식 전부 wrapper 안으로 이동
*/
while (
target.firstChild
) {
wrapper.appendChild(
target.firstChild
);
}
 
 
target.appendChild(
wrapper
);
 
 
$( target ).data(
DATA_WRAPPED,
true
);
 
 
return wrapper;
}
 
 
/**
* 실제 애니메이션 요소 결정
*
*
* 1순위:
* mw-collapsible-content가 있으면 그것,
* .mw-collapsible-content
* 없으면 target 자체.
*
*
* 2순위:
* 이번 방식에서는 DOM wrapper를 새로 만들지 않음.
* 자동 생성 inner wrapper
*/
*/
function getAnimatedElement( target ) {
function getAnimatedElement( target ) {
262번째 줄: 142번째 줄:
}
}


 
return target;
return createInnerWrapper(
target
);
}
 
 
/**
* target 자신의 직계 토글 검색
*
* inner wrapper가 생성된 뒤에는
* 토글이 wrapper 안으로 들어가므로
* wrapper 내부도 확인.
*/
function getOwnToggle(
target,
name
) {
var className =
'mw-customtoggle-' +
name;
 
var children =
target.children;
 
var i;
 
 
for (
i = 0;
i < children.length;
i++
) {
if (
children[ i ].classList &&
children[ i ].classList.contains(
className
)
) {
return children[ i ];
}
}
 
 
var wrapper =
getDirectInnerWrapper(
target
);
 
 
if ( wrapper ) {
children =
wrapper.children;
 
for (
i = 0;
i < children.length;
i++
) {
if (
children[ i ].classList &&
children[ i ].classList.contains(
className
)
) {
return children[ i ];
}
}
}
 
 
return null;
}
}




/**
/**
* 현재 접힌 상태인지 확인
* 현재 접힌 상태
*/
*/
function isCollapsed(
function isCollapsed(
351번째 줄: 160번째 줄:
return true;
return true;
}
}


if (
if (
window
window.getComputedStyle(
.getComputedStyle(
target
target
).display === 'none'
)
.display === 'none'
) {
) {
return true;
return true;
}
}


return (
return (
window
window.getComputedStyle(
.getComputedStyle(
element
element
).display === 'none'
)
.display === 'none'
);
);
}
}
375번째 줄: 178번째 줄:


/**
/**
* display 값 기억
* 현재 display 기억
*/
*/
function rememberDisplay( element ) {
function rememberDisplay( element ) {
var display =
var display =
window
window.getComputedStyle(
.getComputedStyle(
element
element
).display;
)
.display;


if (
if (
398번째 줄: 199번째 줄:


/**
/**
* target 자체 display 기억
* 펼칠 때 display 결정
*/
function rememberTargetDisplay(
target
) {
var display =
window
.getComputedStyle(
target
)
.display;
 
if (
display &&
display !== 'none'
) {
$( target ).data(
DATA_DISPLAY,
display
);
}
}
 
 
/**
* 펼칠 때 사용할 display 계산
*/
*/
function getOpenDisplay( element ) {
function getOpenDisplay( element ) {
441번째 줄: 217번째 줄:
) {
) {
return element.style.display;
return element.style.display;
}
var computed =
window
.getComputedStyle(
element
)
.display;
if (
computed &&
computed !== 'none'
) {
return computed;
}
}




switch ( element.tagName ) {
switch ( element.tagName ) {
case 'TABLE':
case 'TABLE':
return 'table';
return 'table';
481번째 줄: 240번째 줄:
return 'table-cell';
return 'table-cell';


case 'COLGROUP':
case 'LI':
return 'table-column-group';
return 'list-item';
}


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


case 'CAPTION':
/*
return 'table-caption';
* 현재 제공하신 정보상자에서는
* mw-customcollapsible-1이 div이므로 block.
*/
return 'block';
}


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


/**
* 진행 중인 Web Animation 취소
*/
function cancelAnimation( element ) {
var animation =
$( element ).data(
DATA_ANIMATION
);


if (
if ( animation ) {
element.classList.contains(
try {
'mw-collapsible'
animation.cancel();
)
} catch ( e ) {
) {
// 무시
if (
element.style.width === '100%' ||
element.style.width === '100.0%'
) {
return 'block';
}
}


return 'inline-block';
$( element ).removeData(
DATA_ANIMATION
);
}
}
}
/**
* target 내부의 해당 토글 찾기
*/
function getOwnToggle(
target,
name
) {
var className =
'mw-customtoggle-' +
name;
var toggles =
target.getElementsByClassName(
className
);


if ( toggles.length ) {
return toggles[ 0 ];
}


return 'block';
return null;
}
}




/**
/**
* aria-expanded 갱신
* ARIA 상태
*/
*/
function updateAria(
function updateAria(
557번째 줄: 342번째 줄:
hideOwnToggle
hideOwnToggle
) {
) {
var $target =
cancelAnimation(
$( target );
element
);


var $element =
$( element );


var ownToggle =
var ownToggle =
568번째 줄: 352번째 줄:


/*
/*
* 교대형 구조:
* 동일 ID 2개 교대형에서
* 새로 나타나는 쪽 토글 글자를
* 두 토글 글자가 겹치는 현상 방지
* 애니메이션이 끝날 때까지 숨김.
*/
*/
if ( hideOwnToggle ) {
if ( hideOwnToggle ) {
586번째 줄: 369번째 줄:




$element.stop(
target.classList.remove(
true,
'mw-collapsed'
false
);
);




/*
/*
* target 자체가 display:none 상태라면
* target 자체가 display:none이면
* 먼저 바깥 컨테이너를 복원.
* 먼저 화면에 복구
*/
*/
if (
if (
window
window.getComputedStyle(
.getComputedStyle(
target
).display === 'none'
) {
target.style.display =
getOpenDisplay(
target
target
)
);
.display === 'none'
}
 
 
/*
* content 자체가 display:none인 경우
*/
if (
window.getComputedStyle(
element
).display === 'none'
) {
) {
$target.css(
element.style.display =
'display',
getOpenDisplay(
getOpenDisplay(
target
element
)
);
);
}
}




target.classList.remove(
/*
'mw-collapsed'
* 실제 콘텐츠 레이아웃을 먼저 완성.
);
*
* 중요:
* 내부 TABLE 높이는 건드리지 않음.
*/
element.style.height =
'auto';
 
element.style.overflow =
'hidden';
 
element.style.clipPath =
'inset(0 0 0 0)';
 
 
var endHeight =
element.scrollHeight;




/*
/*
* 내부 요소만 slideDown.
* wrapper의 레이아웃 높이만 0으로 설정.
*
*
* target 자체 height는 건드리지 않음.
* 내부 table은 온전한 크기로 계산된 상태.
*/
*/
$element
element.style.height =
.stop(
'0px';
true,
 
false
element.style.clipPath =
)
'inset(0 0 100% 0)';
.slideDown(
 
 
void element.offsetHeight;
 
 
var animation =
element.animate(
[
{
height:
'0px',
 
clipPath:
'inset(0 0 100% 0)'
},
{
height:
endHeight +
'px',
 
clipPath:
'inset(0 0 0% 0)'
}
],
{
{
duration:
duration:
635번째 줄: 466번째 줄:
EASING,
EASING,


complete:
fill:
function () {
'forwards'
}
);
 
 
$( element ).data(
DATA_ANIMATION,
animation
);
 
 
animation.onfinish =
function () {
 
$( element ).removeData(
DATA_ANIMATION
);
 
 
/*
* 최종 상태는 CSS/HTML에 맡김
*/
element.style.height =
'';
 
element.style.overflow =
'';
 
element.style.clipPath =
'';


$element.css(
{
height:
'',


overflow:
if ( ownToggle ) {
''
ownToggle.style.visibility =
}
'';
);
}




if (
try {
ownToggle
animation.cancel();
) {
} catch ( e ) {
ownToggle.style.visibility =
// 무시
'';
}
}
}
}
);
};
}
}


668번째 줄: 521번째 줄:
element
element
) {
) {
var $target =
cancelAnimation(
$( target );
element
 
var $element =
$( element );
 
 
$element.stop(
true,
false
);
);




rememberTargetDisplay(
rememberDisplay(
target
target
);
);
688번째 줄: 533번째 줄:
element
element
);
);
var startHeight =
element.getBoundingClientRect()
.height;




/*
/*
* 내부 요소만 slideUp.
* 내부 table 자체는 그대로 유지.
*/
*/
$element.slideUp(
element.style.height =
{
startHeight +
duration:
'px';
DURATION,


easing:
element.style.overflow =
EASING,
'hidden';


complete:
element.style.clipPath =
function () {
'inset(0 0 0% 0)';


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


void element.offsetHeight;


/*
* 자동 wrapper 구조에서는
* 바깥 target도 최종적으로 숨김.
*
* LACTEA 같은 display:none 교대형
* 구조 대응.
*/
if (
$( target ).data(
DATA_WRAPPED
)
) {
$target.css(
'display',
'none'
);
}


var animation =
element.animate(
[
{
height:
startHeight +
'px',


/*
clipPath:
* 다음에 다시 slideDown할 수 있도록
'inset(0 0 0% 0)'
* 내부 wrapper의 display만 정리.
},
*/
{
$element.css(
height:
{
'0px',
height:
'',


overflow:
clipPath:
''
'inset(0 0 100% 0)'
}
);
}
}
}
],
{
duration:
DURATION,
 
easing:
EASING,
 
fill:
'forwards'
}
);
 
 
$( element ).data(
DATA_ANIMATION,
animation
);
);
animation.onfinish =
function () {
$( element ).removeData(
DATA_ANIMATION
);
target.classList.add(
'mw-collapsed'
);
/*
* target 자체가 content인 구조
*/
if (
element === target
) {
target.style.display =
'none';
} else {
/*
* 일반
* .mw-collapsible-content
*/
element.style.display =
'none';
}
element.style.height =
'';
element.style.overflow =
'';
element.style.clipPath =
'';
try {
animation.cancel();
} catch ( e ) {
// 무시
}
};
}
}




/**
/**
* 그룹 전체 토글
* 그룹 토글
*/
*/
function toggleGroup(
function toggleGroup( toggle ) {
toggle
) {
var name =
var name =
getToggleName(
getToggleName(
781번째 줄: 679번째 줄:
targets.length > 1;
targets.length > 1;


 
var opened =
var openedAny =
false;
false;


795번째 줄: 692번째 줄:
var target =
var target =
targets[ i ];
targets[ i ];


var element =
var element =
801번째 줄: 697번째 줄:
target
target
);
);


var collapsed =
var collapsed =
811번째 줄: 706번째 줄:


if ( collapsed ) {
if ( collapsed ) {
expand(
expand(
target,
target,
819번째 줄: 713번째 줄:
);
);


openedAny =
opened =
true;
true;


} else {
} else {
collapse(
collapse(
target,
target,
837번째 줄: 730번째 줄:
updateAria(
updateAria(
name,
name,
openedAny
opened
);
);
}
}
847번째 줄: 740번째 줄:


/**
/**
* 클릭 처리
* 클릭
*/
*/
document.addEventListener(
document.addEventListener(
937번째 줄: 830번째 줄:


/**
/**
* 초기 상태 저장
* 초기 display 저장
*/
*/
function initializeDisplays(
function initialize(
root
root
) {
) {
955번째 줄: 848번째 줄:
i++
i++
) {
) {
var element =
rememberDisplay(
collapsibles[ i ];
collapsibles[ i ]
 
);
 
var display =
window
.getComputedStyle(
element
)
.display;
 
 
if (
display &&
display !== 'none'
) {
$( element ).data(
DATA_DISPLAY,
display
);
}
 
 
/*
* 이미 mw-collapsible-content가 없는 구조는
* 초기화 시 wrapper를 미리 만들지 않음.
*
* 실제 클릭할 때 필요한 target만 생성해서
* 불필요한 DOM 변경을 줄임.
*/
}
}
}
}




/**
* 최초 로드
*/
$( function () {
$( function () {
initializeDisplays(
initialize(
document
document
);
);
999번째 줄: 862번째 줄:




/**
* MediaWiki 동적 콘텐츠 대응
*/
if (
if (
mw &&
mw &&
1,014번째 줄: 874번째 줄:
$content[ 0 ]
$content[ 0 ]
) {
) {
initializeDisplays(
initialize(
$content[ 0 ]
$content[ 0 ]
);
);

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

/**
 * MediaWiki 커스텀 펼치기 / 접기 애니메이션
 *
 * 지원:
 * - 일반 mw-collapsible-content
 * - mw-collapsible 자체가 display:none 되는 구조
 * - 동일 ID 2개 교대형 구조
 * - flex 형태 복수 토글
 * - 중첩 table / rowspan / infobox
 *
 * 특징:
 * - opacity 없음
 * - 내부 table 자체의 height는 변경하지 않음
 * - wrapper의 height + clip-path만 애니메이션
 */

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

	var DURATION = 180;

	var EASING =
		'cubic-bezier(0.4, 0, 0.2, 1)';

	var DATA_DISPLAY =
		'mwCustomCollapseDisplay';

	var DATA_ANIMATION =
		'mwCustomCollapseAnimation';


	/**
	 * mw-customtoggle-XXX
	 * →
	 * XXX
	 */
	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,
				'\\"'
			);
	}


	/**
	 * 같은 이름의 모든 target
	 */
	function getTargets( name ) {
		var id =
			'mw-customcollapsible-' +
			name;

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


	/**
	 * 직계 mw-collapsible-content 검색
	 */
	function getDirectContent( target ) {
		var children =
			target.children;

		var i;

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

		return null;
	}


	/**
	 * 실제 애니메이션 요소
	 *
	 * mw-collapsible-content가 있으면 그것,
	 * 없으면 target 자체.
	 *
	 * 이번 방식에서는 DOM wrapper를 새로 만들지 않음.
	 */
	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;
		}

		if (
			window.getComputedStyle(
				target
			).display === 'none'
		) {
			return true;
		}

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


	/**
	 * 현재 display 기억
	 */
	function rememberDisplay( element ) {
		var display =
			window.getComputedStyle(
				element
			).display;

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


	/**
	 * 펼칠 때 display 결정
	 */
	function getOpenDisplay( element ) {
		var stored =
			$( element ).data(
				DATA_DISPLAY
			);

		if ( stored ) {
			return stored;
		}


		if (
			element.style.display &&
			element.style.display !== 'none'
		) {
			return element.style.display;
		}


		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 'LI':
				return 'list-item';
		}


		/*
		 * 현재 제공하신 정보상자에서는
		 * mw-customcollapsible-1이 div이므로 block.
		 */
		return 'block';
	}


	/**
	 * 진행 중인 Web Animation 취소
	 */
	function cancelAnimation( element ) {
		var animation =
			$( element ).data(
				DATA_ANIMATION
			);

		if ( animation ) {
			try {
				animation.cancel();
			} catch ( e ) {
				// 무시
			}

			$( element ).removeData(
				DATA_ANIMATION
			);
		}
	}


	/**
	 * target 내부의 해당 토글 찾기
	 */
	function getOwnToggle(
		target,
		name
	) {
		var className =
			'mw-customtoggle-' +
			name;

		var toggles =
			target.getElementsByClassName(
				className
			);

		if ( toggles.length ) {
			return toggles[ 0 ];
		}

		return null;
	}


	/**
	 * ARIA 상태
	 */
	function updateAria(
		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,
		name,
		hideOwnToggle
	) {
		cancelAnimation(
			element
		);


		var ownToggle =
			null;


		/*
		 * 동일 ID 2개 교대형에서
		 * 두 토글 글자가 겹치는 현상 방지
		 */
		if ( hideOwnToggle ) {
			ownToggle =
				getOwnToggle(
					target,
					name
				);

			if ( ownToggle ) {
				ownToggle.style.visibility =
					'hidden';
			}
		}


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


		/*
		 * target 자체가 display:none이면
		 * 먼저 화면에 복구
		 */
		if (
			window.getComputedStyle(
				target
			).display === 'none'
		) {
			target.style.display =
				getOpenDisplay(
					target
				);
		}


		/*
		 * content 자체가 display:none인 경우
		 */
		if (
			window.getComputedStyle(
				element
			).display === 'none'
		) {
			element.style.display =
				getOpenDisplay(
					element
				);
		}


		/*
		 * 실제 콘텐츠 레이아웃을 먼저 완성.
		 *
		 * 중요:
		 * 내부 TABLE 높이는 건드리지 않음.
		 */
		element.style.height =
			'auto';

		element.style.overflow =
			'hidden';

		element.style.clipPath =
			'inset(0 0 0 0)';


		var endHeight =
			element.scrollHeight;


		/*
		 * wrapper의 레이아웃 높이만 0으로 설정.
		 *
		 * 내부 table은 온전한 크기로 계산된 상태.
		 */
		element.style.height =
			'0px';

		element.style.clipPath =
			'inset(0 0 100% 0)';


		void element.offsetHeight;


		var animation =
			element.animate(
				[
					{
						height:
							'0px',

						clipPath:
							'inset(0 0 100% 0)'
					},
					{
						height:
							endHeight +
							'px',

						clipPath:
							'inset(0 0 0% 0)'
					}
				],
				{
					duration:
						DURATION,

					easing:
						EASING,

					fill:
						'forwards'
				}
			);


		$( element ).data(
			DATA_ANIMATION,
			animation
		);


		animation.onfinish =
			function () {

				$( element ).removeData(
					DATA_ANIMATION
				);


				/*
				 * 최종 상태는 CSS/HTML에 맡김
				 */
				element.style.height =
					'';

				element.style.overflow =
					'';

				element.style.clipPath =
					'';


				if ( ownToggle ) {
					ownToggle.style.visibility =
						'';
				}


				try {
					animation.cancel();
				} catch ( e ) {
					// 무시
				}
			};
	}


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


		rememberDisplay(
			target
		);

		rememberDisplay(
			element
		);


		var startHeight =
			element.getBoundingClientRect()
				.height;


		/*
		 * 내부 table 자체는 그대로 유지.
		 */
		element.style.height =
			startHeight +
			'px';

		element.style.overflow =
			'hidden';

		element.style.clipPath =
			'inset(0 0 0% 0)';


		void element.offsetHeight;


		var animation =
			element.animate(
				[
					{
						height:
							startHeight +
							'px',

						clipPath:
							'inset(0 0 0% 0)'
					},
					{
						height:
							'0px',

						clipPath:
							'inset(0 0 100% 0)'
					}
				],
				{
					duration:
						DURATION,

					easing:
						EASING,

					fill:
						'forwards'
				}
			);


		$( element ).data(
			DATA_ANIMATION,
			animation
		);


		animation.onfinish =
			function () {

				$( element ).removeData(
					DATA_ANIMATION
				);


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


				/*
				 * target 자체가 content인 구조
				 */
				if (
					element === target
				) {
					target.style.display =
						'none';

				} else {

					/*
					 * 일반
					 * .mw-collapsible-content
					 */
					element.style.display =
						'none';
				}


				element.style.height =
					'';

				element.style.overflow =
					'';

				element.style.clipPath =
					'';


				try {
					animation.cancel();
				} catch ( e ) {
					// 무시
				}
			};
	}


	/**
	 * 그룹 토글
	 */
	function toggleGroup( toggle ) {
		var name =
			getToggleName(
				toggle
			);


		if ( !name ) {
			return false;
		}


		var targets =
			getTargets(
				name
			);


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


		var multipleTargets =
			targets.length > 1;

		var opened =
			false;

		var i;


		for (
			i = 0;
			i < targets.length;
			i++
		) {
			var target =
				targets[ i ];

			var element =
				getAnimatedElement(
					target
				);

			var collapsed =
				isCollapsed(
					target,
					element
				);


			if ( collapsed ) {
				expand(
					target,
					element,
					name,
					multipleTargets
				);

				opened =
					true;

			} else {
				collapse(
					target,
					element
				);
			}
		}


		if (
			targets.length === 1
		) {
			updateAria(
				name,
				opened
			);
		}


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


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


	/**
	 * 초기 display 저장
	 */
	function initialize(
		root
	) {
		var collapsibles =
			root.querySelectorAll(
				'.mw-collapsible'
			);

		var i;


		for (
			i = 0;
			i < collapsibles.length;
			i++
		) {
			rememberDisplay(
				collapsibles[ i ]
			);
		}
	}


	$( function () {
		initialize(
			document
		);
	} );


	if (
		mw &&
		mw.hook
	) {
		mw.hook(
			'wikipage.content'
		).add(
			function ( $content ) {
				if (
					$content &&
					$content[ 0 ]
				) {
					initialize(
						$content[ 0 ]
					);
				}
			}
		);
	}


}( jQuery, mediaWiki ) );