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

편집 요약 없음
편집 요약 없음
1번째 줄: 1번째 줄:
( function ( $, mw ) {
/**
* MediaWiki 커스텀 펼치기 / 접기 애니메이션
*
* 지원:
* - mw-customtoggle-XXX
* - mw-customcollapsible-XXX
* - 일반 mw-collapsible-content
* - 동일 ID 2개 교대형 구조
* - flex 내부 여러 토글
* - 중첩 표
*
* 효과:
* - 높이 애니메이션만 적용
* - opacity 없음
*/
 
( function ( $ ) {
'use strict';
'use strict';


var DURATION = 180;
var DURATION = 180;
var EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
var EASING = 'swing';
 
var VIEWPORT_CLASS =
'mw-custom-collapse-viewport';
 
var INNER_CLASS =
'mw-custom-collapse-viewport-inner';


var VIEWPORT_CLASS = 'mw-collapse-viewport';
var DATA_DISPLAY =
var CONTENT_CLASS = 'mw-collapse-content';
'mwCustomCollapseOriginalDisplay';




/**
* mw-customtoggle-XXX → XXX
*/
function getToggleName( toggle ) {
function getToggleName( toggle ) {
var prefix = 'mw-customtoggle-';
var prefix =
var classes = toggle.classList;
'mw-customtoggle-';
 
var classes =
toggle.classList;
 
var i;
var i;


for ( i = 0; i < classes.length; i++ ) {
for (
if ( classes[ i ].indexOf( prefix ) === 0 ) {
i = 0;
return classes[ i ].substring( prefix.length );
i < classes.length;
i++
) {
if (
classes[ i ].indexOf(
prefix
) === 0
) {
return classes[ i ].substring(
prefix.length
);
}
}
}
}
24번째 줄: 63번째 줄:




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




/**
* 동일한 이름의 target 모두 검색
*/
function getTargets( name ) {
function getTargets( name ) {
var id = 'mw-customcollapsible-' + name;
var id =
'mw-customcollapsible-' +
name;


return document.querySelectorAll(
return document.querySelectorAll(
'[id="' + escapeAttribute( id ) + '"]'
'[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;
}
/**
* 현재 접힘 상태
*/
function isCollapsed( target ) {
function isCollapsed( target ) {
if (
target.classList.contains(
'mw-collapsed'
)
) {
return true;
}
return (
return (
target.classList.contains( 'mw-collapsed' ) ||
window.getComputedStyle(
window.getComputedStyle( target ).display === 'none'
target
).display === 'none'
);
);
}
}
49번째 줄: 144번째 줄:


/**
/**
* target 내부에 viewport/content 구조 생성
* display 상태 기억
*
*/
* target
function rememberDisplay( target ) {
* └─ viewport
var display =
*   └─ content
window.getComputedStyle(
*       └─ 기존 내용 전체
target
).display;
 
if (
display &&
display !== 'none'
) {
$( target ).data(
DATA_DISPLAY,
display
);
}
}
 
 
/**
* 펼칠 때 target의 display 복원
*/
function getOpenDisplay( target ) {
var saved =
$( target ).data(
DATA_DISPLAY
);
 
if ( saved ) {
return saved;
}
 
 
/*
* HTML에 원래 display 값이 남아 있다면
* 그 값을 우선 사용
*/
if (
target.style.display &&
target.style.display !== 'none'
) {
return target.style.display;
}
 
 
/*
* TABLE 계열
*/
switch ( target.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-collapsible은 대부분 DIV
*/
return 'block';
}
 
 
/**
* 애니메이션용 viewport 검색
*/
*/
function ensureViewport( target ) {
function getViewport( target ) {
var children =
target.children;


var directChildren = target.children;
var i;
var i;


for ( i = 0; i < directChildren.length; i++ ) {
for (
i = 0;
i < children.length;
i++
) {
if (
if (
directChildren[ i ].classList &&
children[ i ].classList &&
directChildren[ i ].classList.contains(
children[ i ].classList.contains(
VIEWPORT_CLASS
VIEWPORT_CLASS
)
)
) {
) {
return directChildren[ i ];
return children[ i ];
}
}
}
}
return null;
}
/**
* 애니메이션용 viewport 생성
*
* 내부 TABLE 자체에는 height를 적용하지 않고
* viewport 높이만 변화시킴.
*/
function ensureViewport( target ) {
var existing =
getViewport(
target
);
if ( existing ) {
return existing;
}
var viewport =
document.createElement(
'div'
);
var inner =
document.createElement(
'div'
);




var viewport = document.createElement( 'div' );
viewport.className =
var content = document.createElement( 'div' );
VIEWPORT_CLASS;


viewport.className = VIEWPORT_CLASS;
inner.className =
content.className = CONTENT_CLASS;
INNER_CLASS;




/*
/*
* 중요:
* MediaWiki 표준 구조라면
* 기존 내용 전체를 content로 이동
* mw-collapsible-content의 내용만 이동
*/
*/
while ( target.firstChild ) {
var content =
getDirectContent(
target
);
 
 
if ( content ) {
 
while (
content.firstChild
) {
inner.appendChild(
content.firstChild
);
}
 
 
viewport.appendChild(
inner
);
 
 
content.appendChild(
content.appendChild(
viewport
);
return viewport;
}
/*
* mw-collapsible-content가 없는 구조
*
* target 내부 전체를 viewport 안으로 이동
*/
while (
target.firstChild
) {
inner.appendChild(
target.firstChild
target.firstChild
);
);
91번째 줄: 338번째 줄:




viewport.appendChild( content );
viewport.appendChild(
target.appendChild( viewport );
inner
);




viewport.style.overflow = 'hidden';
target.appendChild(
viewport
);




102번째 줄: 352번째 줄:




function getContent( viewport ) {
/**
var children = viewport.children;
* viewport inner
*/
function getViewportInner(
viewport
) {
var children =
viewport.children;
 
var i;
var i;


for ( i = 0; i < children.length; i++ ) {
for (
i = 0;
i < children.length;
i++
) {
if (
if (
children[ i ].classList &&
children[ i ].classList &&
children[ i ].classList.contains(
children[ i ].classList.contains(
CONTENT_CLASS
INNER_CLASS
)
)
) {
) {
121번째 줄: 382번째 줄:




function getNaturalDisplay( target ) {
/**
* target 안의 자기 토글 검색
*
* 동일 ID 2개 교대형에서 사용.
*/
function getOwnToggle(
target,
name
) {
var className =
'mw-customtoggle-' +
name;
 
var elements =
target.getElementsByClassName(
className
);


if (
if (
target.dataset.mwCollapseDisplay
elements.length
) {
) {
return target.dataset.mwCollapseDisplay;
return elements[ 0 ];
}
}


return null;
}


var style =
window.getComputedStyle( target );


/**
* 펼치기
*/
function expand(
target,
name,
multipleTargets
) {
var $target =
$( target );


if (
style.display &&
style.display !== 'none'
) {
target.dataset.mwCollapseDisplay =
style.display;


return style.display;
/*
}
* 진행 중인 target animation 중단
*/
$target.stop(
true,
true
);




if (
target.classList.remove(
target.style.width === '100%'
'mw-collapsed'
) {
);
return 'block';
}




return 'block';
/*
}
* 바깥 target 먼저 표시
 
*/
 
target.style.display =
function rememberDisplay( target ) {
getOpenDisplay(
 
var display =
window.getComputedStyle(
target
target
).display;
);


if (
display &&
display !== 'none'
) {
target.dataset.mwCollapseDisplay =
display;
}
}
function expand( target ) {


var viewport =
var viewport =
181번째 줄: 450번째 줄:
);
);


var content =
 
getContent(
var inner =
getViewportInner(
viewport
viewport
);
);




if ( !content ) {
if ( !inner ) {
return;
return;
}
}
var ownToggle =
null;




/*
/*
* target 바깥쪽만 표시
* 동일 ID가 2개인 교대형 구조:
*
* 접히는 버튼과 펼쳐지는 버튼이
* 애니메이션 중 겹치지 않도록 처리.
*/
*/
target.style.display =
if ( multipleTargets ) {
getNaturalDisplay(
ownToggle =
target
getOwnToggle(
);
target,
name
);
 
if ( ownToggle ) {
ownToggle.style.visibility =
'hidden';
}
}
 
 
var $viewport =
$( viewport );
 


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




/*
viewport.style.display =
* content 자체는 항상 auto height.
'block';
*
* 내부 table에는 height를 절대 설정하지 않음.
*/
content.style.height = 'auto';


viewport.style.overflow =
'hidden';


viewport.style.height = 'auto';
viewport.style.height =
viewport.style.overflow = 'hidden';
'auto';




var finalHeight =
/*
content.getBoundingClientRect()
* 내부 table은 건드리지 않고
* inner의 실제 높이를 측정
*/
var targetHeight =
inner.getBoundingClientRect()
.height;
.height;




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




/*
* 강제 reflow
*/
void viewport.offsetHeight;
void viewport.offsetHeight;




var animation =
$viewport.animate(
viewport.animate(
{
[
height:
{
targetHeight
height: '0px'
},
},
{
{
duration:
height:
DURATION,
finalHeight +
'px'
}
],
{
duration:
DURATION,


easing:
easing:
EASING,
EASING,


fill:
complete:
'forwards'
function () {
}
);


viewport.style.height =
'';


animation.onfinish =
viewport.style.overflow =
function () {
'';


viewport.style.height =
'';


viewport.style.overflow =
if (
'';
ownToggle
 
) {
animation.cancel();
ownToggle.style.visibility =
};
'';
}
}
}
);
}
}




function collapse( target ) {
/**
 
* 접기
*/
function collapse(
target
) {
rememberDisplay(
rememberDisplay(
target
target
279번째 줄: 575번째 줄:
);
);


var content =
 
getContent(
var inner =
getViewportInner(
viewport
viewport
);
);




if ( !content ) {
if ( !inner ) {
return;
return;
}
}
var $viewport =
$( viewport );
$viewport.stop(
true,
true
);
var currentHeight =
viewport.getBoundingClientRect()
.height;




/*
/*
* 내부 table은 그대로 둠.
* 혹시 viewport 높이가 0으로 잡히는 경우
* viewport 높이만 줄임.
* inner 실제 높이 사용
*/
*/
var startHeight =
if (
content.getBoundingClientRect()
currentHeight <= 0
.height;
) {
currentHeight =
inner.getBoundingClientRect()
.height;
}




viewport.style.height =
viewport.style.height =
startHeight +
currentHeight +
'px';
'px';


310번째 줄: 626번째 줄:




var animation =
$viewport.animate(
viewport.animate(
{
[
height:
{
0
height:
},
startHeight +
{
'px'
duration:
},
DURATION,
{
 
height:
easing:
'0px'
EASING,
}
 
],
complete:
{
function () {
duration:
 
DURATION,
target.classList.add(
'mw-collapsed'
);
 


easing:
target.style.display =
EASING,
'none';


fill:
'forwards'
}
);


viewport.style.height =
'';


animation.onfinish =
viewport.style.overflow =
function () {
'';
}
}
);
}


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


target.style.display =
/**
'none';
* ARIA 갱신
*/
function updateAria(
name,
expanded
) {
var className =
'mw-customtoggle-' +
name;
 


var toggles =
document.getElementsByClassName(
className
);


viewport.style.height =
'';


viewport.style.overflow =
var i;
'';




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




function toggleGroup( toggle ) {
/**
 
* 그룹 토글
*/
function toggleGroup(
toggle
) {
var name =
var name =
getToggleName(
getToggleName(
368번째 줄: 710번째 줄:


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


376번째 줄: 718번째 줄:
name
name
);
);
if (
!targets.length
) {
return false;
}
var multipleTargets =
targets.length > 1;




var i;
var i;
var opened =
false;
/*
* 클릭 직전의 상태를 먼저 기록
*
* 각 요소를 순차 수정하다가
* 다음 요소의 상태 판별이 꼬이지 않도록 함.
*/
var states =
[];




386번째 줄: 752번째 줄:
i++
i++
) {
) {
states.push(
isCollapsed(
targets[ i ]
)
);
}


var target =
targets[ i ];


/*
* 상태 변경
*/
for (
i = 0;
i < targets.length;
i++
) {


if (
if (
isCollapsed(
states[ i ]
target
)
) {
) {
expand(
expand(
target
targets[ i ],
name,
multipleTargets
);
);
opened =
true;


} else {
} else {
collapse(
collapse(
target
targets[ i ]
);
);
}
}
}
}
/*
* 일반 target 하나짜리만
* aria-expanded 갱신.
*/
if (
targets.length === 1
) {
updateAria(
name,
opened
);
}
return true;
}
}




/**
* 클릭
*/
document.addEventListener(
document.addEventListener(
'click',
'click',
434번째 줄: 836번째 줄:


event.preventDefault();
event.preventDefault();
event.stopPropagation();
event.stopPropagation();
event.stopImmediatePropagation();
event.stopImmediatePropagation();


446번째 줄: 850번째 줄:




/**
* Enter / Space
*/
document.addEventListener(
document.addEventListener(
'keydown',
'keydown',
465번째 줄: 872번째 줄:


if ( !toggle ) {
if ( !toggle ) {
return;
}
if (
!getToggleName(
toggle
)
) {
return;
return;
}
}
470번째 줄: 886번째 줄:


event.preventDefault();
event.preventDefault();
event.stopPropagation();
event.stopPropagation();
event.stopImmediatePropagation();
event.stopImmediatePropagation();


482번째 줄: 900번째 줄:




}( jQuery, mediaWiki ) );
/**
* 페이지 최초 display 상태 기억
*/
function initialize(
root
) {
var elements =
root.querySelectorAll(
'.mw-collapsible'
);
 
 
var i;
 
 
for (
i = 0;
i < elements.length;
i++
) {
 
if (
window.getComputedStyle(
elements[ i ]
).display !== 'none'
) {
rememberDisplay(
elements[ i ]
);
}
}
}
 
 
$( function () {
initialize(
document
);
} );
 
 
/*
* MediaWiki AJAX / 동적 콘텐츠
*/
if (
window.mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function ( $content ) {
 
if (
$content &&
$content[ 0 ]
) {
initialize(
$content[ 0 ]
);
}
}
);
}
 
 
}( jQuery ) );

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

/**
 * MediaWiki 커스텀 펼치기 / 접기 애니메이션
 *
 * 지원:
 * - mw-customtoggle-XXX
 * - mw-customcollapsible-XXX
 * - 일반 mw-collapsible-content
 * - 동일 ID 2개 교대형 구조
 * - flex 내부 여러 토글
 * - 중첩 표
 *
 * 효과:
 * - 높이 애니메이션만 적용
 * - opacity 없음
 */

( function ( $ ) {
	'use strict';

	var DURATION = 180;
	var EASING = 'swing';

	var VIEWPORT_CLASS =
		'mw-custom-collapse-viewport';

	var INNER_CLASS =
		'mw-custom-collapse-viewport-inner';

	var DATA_DISPLAY =
		'mwCustomCollapseOriginalDisplay';


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


	/**
	 * 현재 접힘 상태
	 */
	function isCollapsed( target ) {
		if (
			target.classList.contains(
				'mw-collapsed'
			)
		) {
			return true;
		}

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


	/**
	 * display 상태 기억
	 */
	function rememberDisplay( target ) {
		var display =
			window.getComputedStyle(
				target
			).display;

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


	/**
	 * 펼칠 때 target의 display 복원
	 */
	function getOpenDisplay( target ) {
		var saved =
			$( target ).data(
				DATA_DISPLAY
			);

		if ( saved ) {
			return saved;
		}


		/*
		 * HTML에 원래 display 값이 남아 있다면
		 * 그 값을 우선 사용
		 */
		if (
			target.style.display &&
			target.style.display !== 'none'
		) {
			return target.style.display;
		}


		/*
		 * TABLE 계열
		 */
		switch ( target.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-collapsible은 대부분 DIV
		 */
		return 'block';
	}


	/**
	 * 애니메이션용 viewport 검색
	 */
	function getViewport( target ) {
		var children =
			target.children;

		var i;

		for (
			i = 0;
			i < children.length;
			i++
		) {
			if (
				children[ i ].classList &&
				children[ i ].classList.contains(
					VIEWPORT_CLASS
				)
			) {
				return children[ i ];
			}
		}

		return null;
	}


	/**
	 * 애니메이션용 viewport 생성
	 *
	 * 내부 TABLE 자체에는 height를 적용하지 않고
	 * viewport 높이만 변화시킴.
	 */
	function ensureViewport( target ) {
		var existing =
			getViewport(
				target
			);

		if ( existing ) {
			return existing;
		}


		var viewport =
			document.createElement(
				'div'
			);

		var inner =
			document.createElement(
				'div'
			);


		viewport.className =
			VIEWPORT_CLASS;

		inner.className =
			INNER_CLASS;


		/*
		 * MediaWiki 표준 구조라면
		 * mw-collapsible-content의 내용만 이동
		 */
		var content =
			getDirectContent(
				target
			);


		if ( content ) {

			while (
				content.firstChild
			) {
				inner.appendChild(
					content.firstChild
				);
			}


			viewport.appendChild(
				inner
			);


			content.appendChild(
				viewport
			);


			return viewport;
		}


		/*
		 * mw-collapsible-content가 없는 구조
		 *
		 * target 내부 전체를 viewport 안으로 이동
		 */
		while (
			target.firstChild
		) {
			inner.appendChild(
				target.firstChild
			);
		}


		viewport.appendChild(
			inner
		);


		target.appendChild(
			viewport
		);


		return viewport;
	}


	/**
	 * viewport inner
	 */
	function getViewportInner(
		viewport
	) {
		var children =
			viewport.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 안의 자기 토글 검색
	 *
	 * 동일 ID 2개 교대형에서 사용.
	 */
	function getOwnToggle(
		target,
		name
	) {
		var className =
			'mw-customtoggle-' +
			name;

		var elements =
			target.getElementsByClassName(
				className
			);

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

		return null;
	}


	/**
	 * 펼치기
	 */
	function expand(
		target,
		name,
		multipleTargets
	) {
		var $target =
			$( target );


		/*
		 * 진행 중인 target animation 중단
		 */
		$target.stop(
			true,
			true
		);


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


		/*
		 * 바깥 target 먼저 표시
		 */
		target.style.display =
			getOpenDisplay(
				target
			);


		var viewport =
			ensureViewport(
				target
			);


		var inner =
			getViewportInner(
				viewport
			);


		if ( !inner ) {
			return;
		}


		var ownToggle =
			null;


		/*
		 * 동일 ID가 2개인 교대형 구조:
		 *
		 * 접히는 버튼과 펼쳐지는 버튼이
		 * 애니메이션 중 겹치지 않도록 처리.
		 */
		if ( multipleTargets ) {
			ownToggle =
				getOwnToggle(
					target,
					name
				);

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


		var $viewport =
			$( viewport );


		$viewport.stop(
			true,
			true
		);


		viewport.style.display =
			'block';

		viewport.style.overflow =
			'hidden';

		viewport.style.height =
			'auto';


		/*
		 * 내부 table은 건드리지 않고
		 * inner의 실제 높이를 측정
		 */
		var targetHeight =
			inner.getBoundingClientRect()
				.height;


		viewport.style.height =
			'0px';


		/*
		 * 강제 reflow
		 */
		void viewport.offsetHeight;


		$viewport.animate(
			{
				height:
					targetHeight
			},
			{
				duration:
					DURATION,

				easing:
					EASING,

				complete:
					function () {

						viewport.style.height =
							'';

						viewport.style.overflow =
							'';


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


	/**
	 * 접기
	 */
	function collapse(
		target
	) {
		rememberDisplay(
			target
		);


		var viewport =
			ensureViewport(
				target
			);


		var inner =
			getViewportInner(
				viewport
			);


		if ( !inner ) {
			return;
		}


		var $viewport =
			$( viewport );


		$viewport.stop(
			true,
			true
		);


		var currentHeight =
			viewport.getBoundingClientRect()
				.height;


		/*
		 * 혹시 viewport 높이가 0으로 잡히는 경우
		 * inner 실제 높이 사용
		 */
		if (
			currentHeight <= 0
		) {
			currentHeight =
				inner.getBoundingClientRect()
					.height;
		}


		viewport.style.height =
			currentHeight +
			'px';

		viewport.style.overflow =
			'hidden';


		void viewport.offsetHeight;


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

				easing:
					EASING,

				complete:
					function () {

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


						target.style.display =
							'none';


						viewport.style.height =
							'';

						viewport.style.overflow =
							'';
					}
			}
		);
	}


	/**
	 * 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 toggleGroup(
		toggle
	) {
		var name =
			getToggleName(
				toggle
			);


		if ( !name ) {
			return false;
		}


		var targets =
			getTargets(
				name
			);


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


		var multipleTargets =
			targets.length > 1;


		var i;

		var opened =
			false;


		/*
		 * 클릭 직전의 상태를 먼저 기록
		 *
		 * 각 요소를 순차 수정하다가
		 * 다음 요소의 상태 판별이 꼬이지 않도록 함.
		 */
		var states =
			[];


		for (
			i = 0;
			i < targets.length;
			i++
		) {
			states.push(
				isCollapsed(
					targets[ i ]
				)
			);
		}


		/*
		 * 상태 변경
		 */
		for (
			i = 0;
			i < targets.length;
			i++
		) {

			if (
				states[ i ]
			) {
				expand(
					targets[ i ],
					name,
					multipleTargets
				);

				opened =
					true;

			} else {

				collapse(
					targets[ i ]
				);
			}
		}


		/*
		 * 일반 target 하나짜리만
		 * aria-expanded 갱신.
		 */
		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 elements =
			root.querySelectorAll(
				'.mw-collapsible'
			);


		var i;


		for (
			i = 0;
			i < elements.length;
			i++
		) {

			if (
				window.getComputedStyle(
					elements[ i ]
				).display !== 'none'
			) {
				rememberDisplay(
					elements[ i ]
				);
			}
		}
	}


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


	/*
	 * MediaWiki AJAX / 동적 콘텐츠
	 */
	if (
		window.mw &&
		mw.hook
	) {
		mw.hook(
			'wikipage.content'
		).add(
			function ( $content ) {

				if (
					$content &&
					$content[ 0 ]
				) {
					initialize(
						$content[ 0 ]
					);
				}
			}
		);
	}


}( jQuery ) );