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

편집 요약 없음
편집 요약 없음
4번째 줄: 4번째 줄:
  * 지원:
  * 지원:
  *
  *
  * 1. 일반 구조
  * 1. 일반 MediaWiki 구조
  *
  *
  * .mw-customtoggle-XXX
  * .mw-customtoggle-XXX
27번째 줄: 27번째 줄:
  * TWICE_ETC
  * TWICE_ETC
  * TWICE_REL
  * TWICE_REL
*
*
* 4. 정보상자 / table / rowspan 내부 지원
*
* mw-collapsible 자체의 height를 직접 변경하지 않고
* 내부 wrapper만 slide 처리하여
* table 레이아웃 재계산 문제를 줄임.
  *
  *
  *
  *
42번째 줄: 49번째 줄:
var DATA_DISPLAY =
var DATA_DISPLAY =
'mwCustomCollapseDisplay';
'mwCustomCollapseDisplay';
var INNER_CLASS =
'mw-custom-collapse-inner';
var DATA_WRAPPED =
'mwCustomCollapseWrapped';




97번째 줄: 110번째 줄:


/**
/**
* 같은 name에 해당하는 모든 target 검색
* 같은 name을 가진 모든 target 검색
*
* 동일 ID가 여러 개 있어도 전부 가져옴.
*/
*/
function getTargets( name ) {
function getTargets( name ) {
122번째 줄: 133번째 줄:
* .mw-collapsible-content 검색
* .mw-collapsible-content 검색
*
*
* 중첩된 collapsible의 content를
* 중첩 collapsible 오선택 방지.
* 잘못 잡지 않도록 children만 검사.
*/
*/
function getDirectContent( target ) {
function getDirectContent( target ) {
137번째 줄: 147번째 줄:
) {
) {
if (
if (
children[ i ]
children[ i ].classList &&
.classList
children[ i ].classList.contains(
.contains(
'mw-collapsible-content'
'mw-collapsible-content'
)
)
) {
return children[ i ];
}
}
 
return null;
}
 
 
/**
* 기존에 만든 내부 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 children[ i ];
152번째 줄: 189번째 줄:


/**
/**
* 실제 애니메이션할 요소
* 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;
}
 
 
/**
* 실제 애니메이션 요소 결정
*
*
* 일반 MediaWiki:
* 1순위:
* .mw-collapsible-content
* .mw-collapsible-content
*
*
* 교대형:
* 2순위:
* target 자체
* 자동 생성 inner wrapper
*/
*/
function getAnimatedElement( target ) {
function getAnimatedElement( target ) {
170번째 줄: 262번째 줄:
}
}


return target;
 
return createInnerWrapper(
target
);
}
}




/**
/**
* target 자신의 직계 자식 중
* target 자신의 직계 토글 검색
* 해당 name의 토글 찾기
*
*
* 교대형 구조에서 펼쳐지는 쪽의
* inner wrapper가 생성된 뒤에는
* 버튼 텍스트를 잠시 숨기기 위해 사용.
* 토글이 wrapper 안으로 들어가므로
* wrapper 내부도 확인.
*/
*/
function getOwnToggle(
function getOwnToggle(
193번째 줄: 288번째 줄:


var i;
var i;


for (
for (
208번째 줄: 304번째 줄:
}
}
}
}
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;
return null;
227번째 줄: 351번째 줄:
return true;
return true;
}
}
if (
window
.getComputedStyle(
target
)
.display === 'none'
) {
return true;
}


return (
return (
239번째 줄: 375번째 줄:


/**
/**
* 현재 display값 기억
* display 값 기억
*
* inline-block 등을 다시 복원하기 위해 사용.
*/
*/
function rememberDisplay( element ) {
function rememberDisplay( element ) {
256번째 줄: 390번째 줄:
) {
) {
$( element ).data(
$( element ).data(
DATA_DISPLAY,
display
);
}
}
/**
* target 자체 display 기억
*/
function rememberTargetDisplay(
target
) {
var display =
window
.getComputedStyle(
target
)
.display;
if (
display &&
display !== 'none'
) {
$( target ).data(
DATA_DISPLAY,
DATA_DISPLAY,
display
display
267번째 줄: 426번째 줄:
*/
*/
function getOpenDisplay( element ) {
function getOpenDisplay( element ) {
var stored =
var stored =
$( element ).data(
$( element ).data(
278번째 줄: 436번째 줄:




/*
* 유효한 인라인 display가 있으면 사용
*/
if (
if (
element.style.display &&
element.style.display &&
340번째 줄: 495번째 줄:




/*
* mw-collapsible 자체인 경우
*
* 100% 폭이면 block,
* 그 외에는 inline-block 우선.
*/
if (
if (
element.classList.contains(
element.classList.contains(
351번째 줄: 500번째 줄:
)
)
) {
) {
if (
if (
element.style.width === '100%' ||
element.style.width === '100%' ||
368번째 줄: 516번째 줄:


/**
/**
* 같은 name을 가진 모든 toggle의
* aria-expanded 갱신
* aria-expanded 변경
*/
*/
function updateAria(
function updateAria(
403번째 줄: 550번째 줄:
/**
/**
* 펼치기
* 펼치기
*
* hideOwnToggle이 true인 경우
* 펼쳐지는 쪽의 버튼 텍스트는
* 애니메이션 종료까지 visibility:hidden.
*
* 공간 자체는 유지되므로
* 레이아웃이 갑자기 변하지 않음.
*/
*/
function expand(
function expand(
417번째 줄: 557번째 줄:
hideOwnToggle
hideOwnToggle
) {
) {
var $target =
$( target );
var $element =
var $element =
$( element );
$( element );
424번째 줄: 567번째 줄:




if (
/*
hideOwnToggle &&
* 교대형 구조:
element === target
* 새로 나타나는 쪽 토글 글자를
) {
* 애니메이션이 끝날 때까지 숨김.
*/
if ( hideOwnToggle ) {
ownToggle =
ownToggle =
getOwnToggle(
getOwnToggle(
447번째 줄: 592번째 줄:




target.classList.remove(
/*
'mw-collapsed'
* target 자체가 display:none 상태라면
);
* 먼저 바깥 컨테이너를 복원.
 
 
/**
* 일반 MediaWiki 구조
*
* target
* └─ .mw-collapsible-content
*/
*/
if (
if (
element !== target
window
.getComputedStyle(
target
)
.display === 'none'
) {
) {
 
$target.css(
$element
'display',
.stop(
getOpenDisplay(
true,
target
false
)
)
.slideDown(
);
{
duration:
DURATION,
 
easing:
EASING,
 
complete:
function () {
 
$element.css(
{
height:
'',
 
overflow:
''
}
);
 
 
if (
ownToggle
) {
ownToggle.style.visibility =
'';
}
}
}
);
 
return;
}
}




/**
target.classList.remove(
* target 자체가 display:none인 구조
'mw-collapsed'
*/
var display =
getOpenDisplay(
element
);
 
 
$element.css(
{
display:
display,
 
height:
'auto',
 
overflow:
'hidden'
}
);
 
 
var targetHeight =
element.scrollHeight;
 
 
$element.css(
{
height:
'0px'
}
);
);




/*
/*
* 강제 reflow
* 내부 요소만 slideDown.
*
* target 자체 height는 건드리지 않음.
*/
*/
void element.offsetHeight;
$element
.stop(
true,
false
)
.slideDown(
{
duration:
DURATION,


easing:
EASING,


$element.animate(
complete:
{
function () {
height:
targetHeight
},
{
duration:
DURATION,


easing:
$element.css(
EASING,
{
height:
'',


complete:
overflow:
function () {
''
}
);


$element.css(
{
height:
'',


overflow:
if (
''
ownToggle
) {
ownToggle.style.visibility =
'';
}
}
);
/*
* 펼침이 완전히 끝난 뒤
* 내부 버튼 표시
*/
if (
ownToggle
) {
ownToggle.style.visibility =
'';
}
}
}
}
}
);
);
}
}


593번째 줄: 668번째 줄:
element
element
) {
) {
var $target =
$( target );
var $element =
var $element =
$( element );
$( element );
602번째 줄: 680번째 줄:
);
);


rememberTargetDisplay(
target
);


rememberDisplay(
rememberDisplay(
608번째 줄: 690번째 줄:




/**
/*
* 일반 MediaWiki 구조
* 내부 요소만 slideUp.
*/
if (
element !== target
) {
 
$element.slideUp(
{
duration:
DURATION,
 
easing:
EASING,
 
complete:
function () {
 
target.classList.add(
'mw-collapsed'
);
 
 
$element.css(
{
height:
'',
 
overflow:
''
}
);
}
}
);
 
return;
}
 
 
/**
* target 자체가 접히는 구조
*/
*/
var startHeight =
$element.slideUp(
element
.getBoundingClientRect()
.height;
 
 
$element.css(
{
height:
startHeight +
'px',
 
overflow:
'hidden'
}
);
 
 
void element.offsetHeight;
 
 
$element.animate(
{
height:
0
},
{
{
duration:
duration:
692번째 줄: 709번째 줄:




/*
* 자동 wrapper 구조에서는
* 바깥 target도 최종적으로 숨김.
*
* LACTEA 같은 display:none 교대형
* 구조 대응.
*/
if (
$( target ).data(
DATA_WRAPPED
)
) {
$target.css(
'display',
'none'
);
}
/*
* 다음에 다시 slideDown할 수 있도록
* 내부 wrapper의 display만 정리.
*/
$element.css(
$element.css(
{
{
display:
'none',
height:
height:
'',
'',
712번째 줄: 749번째 줄:
/**
/**
* 그룹 전체 토글
* 그룹 전체 토글
*
* target이 하나라면 일반 토글.
*
* 동일 ID target이 여러 개라면
* 각 요소를 현재 상태 반대로 변경.
*
* LACTEA:
*
* A = 상단 버튼
* B = 실제 내용
*
* 클릭 전:
*
* A 열림
* B 닫힘
*
* 클릭 후:
*
* A 닫힘
* B 열림
*/
*/
function toggleGroup(
function toggleGroup(
765번째 줄: 782번째 줄:




var i;
var openedAny =
var openedAny =
false;
false;
var i;




775번째 줄: 793번째 줄:
i++
i++
) {
) {
var target =
var target =
targets[ i ];
targets[ i ];
795번째 줄: 812번째 줄:
if ( collapsed ) {
if ( collapsed ) {


/*
* 동일 ID가 여러 개인 경우
* 새로 펼쳐지는 쪽 토글을
* 애니메이션 도중 숨김.
*
* 따라서
*
* [ 음반 목록 ]
* [ 음반 목록 ]
*
* 처럼 겹쳐 보이지 않음.
*/
expand(
expand(
target,
target,
813번째 줄: 818번째 줄:
multipleTargets
multipleTargets
);
);


openedAny =
openedAny =
828번째 줄: 832번째 줄:




/*
* 일반적인 target 1개 구조만
* ARIA 상태를 일괄 갱신.
*
* 교대형은 두 target의 상태가
* 서로 반대이므로 제외.
*/
if (
if (
targets.length === 1
targets.length === 1
) {
) {
updateAria(
updateAria(
name,
name,
851번째 줄: 847번째 줄:


/**
/**
* 클릭
* 클릭 처리
*
* capture=true를 사용해서
* MediaWiki 기본 customtoggle보다
* 먼저 처리.
*/
*/
document.addEventListener(
document.addEventListener(
'click',
'click',
function ( event ) {
function ( event ) {
var toggle =
var toggle =
event.target.closest(
event.target.closest(
902번째 줄: 893번째 줄:
'keydown',
'keydown',
function ( event ) {
function ( event ) {
if (
if (
event.key !== 'Enter' &&
event.key !== 'Enter' &&
947번째 줄: 937번째 줄:


/**
/**
* 최초 display 상태 저장
* 초기 상태 저장
*
* display:inline-block 같은 값을
* 나중에 정확히 복원하기 위한 처리.
*/
*/
function initializeDisplays(
function initializeDisplays(
968번째 줄: 955번째 줄:
i++
i++
) {
) {
var element =
var element =
collapsibles[ i ];
collapsibles[ i ];
985번째 줄: 971번째 줄:
display !== 'none'
display !== 'none'
) {
) {
$( element ).data(
$( element ).data(
DATA_DISPLAY,
DATA_DISPLAY,
991번째 줄: 976번째 줄:
);
);
}
}
/*
* 이미 mw-collapsible-content가 없는 구조는
* 초기화 시 wrapper를 미리 만들지 않음.
*
* 실제 클릭할 때 필요한 target만 생성해서
* 불필요한 DOM 변경을 줄임.
*/
}
}
}
}
996번째 줄: 990번째 줄:


/**
/**
* 최초 페이지 로드
* 최초 로드
*/
*/
$( function () {
$( function () {
initializeDisplays(
initializeDisplays(
document
document
);
);
} );
} );


1,009번째 줄: 1,001번째 줄:
/**
/**
* MediaWiki 동적 콘텐츠 대응
* MediaWiki 동적 콘텐츠 대응
*
* AJAX / Lua / 확장기능 등으로
* 나중에 들어온 콘텐츠도 처리.
*/
*/
if (
if (
1,017번째 줄: 1,006번째 줄:
mw.hook
mw.hook
) {
) {
mw.hook(
mw.hook(
'wikipage.content'
'wikipage.content'
).add(
).add(
function ( $content ) {
function ( $content ) {
if (
if (
$content &&
$content &&
$content[ 0 ]
$content[ 0 ]
) {
) {
initializeDisplays(
initializeDisplays(
$content[ 0 ]
$content[ 0 ]

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

/**
 * MediaWiki 커스텀 펼치기 / 접기 애니메이션
 *
 * 지원:
 *
 * 1. 일반 MediaWiki 구조
 *
 * .mw-customtoggle-XXX
 * #mw-customcollapsible-XXX
 * └─ .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 사용하지 않음
 */

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

	var DURATION = 200;
	var EASING = 'swing';

	var DATA_DISPLAY =
		'mwCustomCollapseDisplay';

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

	var DATA_WRAPPED =
		'mwCustomCollapseWrapped';


	/**
	 * 토글 클래스에서 식별자 추출
	 *
	 * mw-customtoggle-Flora_LACTEA_01
	 * →
	 * Flora_LACTEA_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;
	}


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


	/**
	 * 같은 name을 가진 모든 target 검색
	 */
	function getTargets( name ) {
		if ( !name ) {
			return [];
		}

		var id =
			'mw-customcollapsible-' +
			name;

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


	/**
	 * target의 직계
	 * .mw-collapsible-content 검색
	 *
	 * 중첩 collapsible 오선택 방지.
	 */
	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;
	}


	/**
	 * 기존에 만든 내부 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
	 *
	 * 2순위:
	 * 자동 생성 inner wrapper
	 */
	function getAnimatedElement( target ) {
		var content =
			getDirectContent(
				target
			);

		if ( content ) {
			return content;
		}


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


	/**
	 * target 자체 display 기억
	 */
	function rememberTargetDisplay(
		target
	) {
		var display =
			window
				.getComputedStyle(
					target
				)
				.display;

		if (
			display &&
			display !== 'none'
		) {
			$( target ).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;
		}


		var computed =
			window
				.getComputedStyle(
					element
				)
				.display;


		if (
			computed &&
			computed !== 'none'
		) {
			return computed;
		}


		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';
		}


		if (
			element.classList.contains(
				'mw-collapsible'
			)
		) {
			if (
				element.style.width === '100%' ||
				element.style.width === '100.0%'
			) {
				return 'block';
			}

			return 'inline-block';
		}


		return 'block';
	}


	/**
	 * aria-expanded 갱신
	 */
	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
	) {
		var $target =
			$( target );

		var $element =
			$( element );

		var ownToggle =
			null;


		/*
		 * 교대형 구조:
		 * 새로 나타나는 쪽 토글 글자를
		 * 애니메이션이 끝날 때까지 숨김.
		 */
		if ( hideOwnToggle ) {
			ownToggle =
				getOwnToggle(
					target,
					name
				);

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


		$element.stop(
			true,
			false
		);


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


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


		/*
		 * 내부 요소만 slideDown.
		 *
		 * target 자체 height는 건드리지 않음.
		 */
		$element
			.stop(
				true,
				false
			)
			.slideDown(
				{
					duration:
						DURATION,

					easing:
						EASING,

					complete:
						function () {

							$element.css(
								{
									height:
										'',

									overflow:
										''
								}
							);


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


	/**
	 * 접기
	 */
	function collapse(
		target,
		element
	) {
		var $target =
			$( target );

		var $element =
			$( element );


		$element.stop(
			true,
			false
		);


		rememberTargetDisplay(
			target
		);

		rememberDisplay(
			element
		);


		/*
		 * 내부 요소만 slideUp.
		 */
		$element.slideUp(
			{
				duration:
					DURATION,

				easing:
					EASING,

				complete:
					function () {

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


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


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

								overflow:
									''
							}
						);
					}
			}
		);
	}


	/**
	 * 그룹 전체 토글
	 */
	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 openedAny =
			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
				);

				openedAny =
					true;

			} else {

				collapse(
					target,
					element
				);
			}
		}


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


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


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

		var i;


		for (
			i = 0;
			i < collapsibles.length;
			i++
		) {
			var element =
				collapsibles[ i ];


			var display =
				window
					.getComputedStyle(
						element
					)
					.display;


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


			/*
			 * 이미 mw-collapsible-content가 없는 구조는
			 * 초기화 시 wrapper를 미리 만들지 않음.
			 *
			 * 실제 클릭할 때 필요한 target만 생성해서
			 * 불필요한 DOM 변경을 줄임.
			 */
		}
	}


	/**
	 * 최초 로드
	 */
	$( function () {
		initializeDisplays(
			document
		);
	} );


	/**
	 * MediaWiki 동적 콘텐츠 대응
	 */
	if (
		mw &&
		mw.hook
	) {
		mw.hook(
			'wikipage.content'
		).add(
			function ( $content ) {
				if (
					$content &&
					$content[ 0 ]
				) {
					initializeDisplays(
						$content[ 0 ]
					);
				}
			}
		);
	}


}( jQuery, mediaWiki ) );