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

편집 요약 없음
편집 요약 없음
2번째 줄: 2번째 줄:
  * MediaWiki 커스텀 펼치기 / 접기 애니메이션
  * MediaWiki 커스텀 펼치기 / 접기 애니메이션
  *
  *
  * 지원:
  * 지원 구조
  *
  *
  * 1. 일반 MediaWiki 구조
  * 1. 일반 MediaWiki 구조
  *
  *
  * .mw-customtoggle-XXX
  * <div class="mw-customtoggle-XXX mw-customtoggle">
  * #mw-customcollapsible-XXX
  *     [ 펼치기 · 접기 ]
  * └─ .mw-collapsible-content
  * </div>
  *
  *
* <div
*    id="mw-customcollapsible-XXX"
*    class="mw-collapsible mw-collapsed"
* >
*    <div class="mw-collapsible-content">
*        내용
*    </div>
* </div>
  *
  *
* 2. 동일 ID를 2개 사용하는 전환형 둘러보기 틀
  *
  *
  * #mw-customcollapsible-XXX  ← 처음 보이는 버튼
  * 2. 동일 ID를 2개 사용하는 전환형 구조
* └─ .mw-customtoggle-XXX
  *
  *
  * #mw-customcollapsible-XXX   ← 처음 숨겨진 실제 내용
  * <div
  * ├─ .mw-customtoggle-XXX
*    id="mw-customcollapsible-XXX"
  * └─ table / .sakura-table-wrapper
*    class="mw-collapsible"
* >
  *     <div class="mw-customtoggle-XXX mw-customtoggle">
*        [ 버튼 ]
  *     </div>
* </div>
  *
  *
* <div
*    id="mw-customcollapsible-XXX"
*    class="mw-collapsible mw-collapsed"
*    style="display:none;"
* >
*    <div class="mw-customtoggle-XXX mw-customtoggle">
*        [ 버튼 ]
*    </div>
  *
  *
  * 클릭 시 같은 XXX를 가진 모든 collapsible을
  *     실제 내용
  * 각각 현재 상태의 반대로 전환합니다.
  * </div>
*
*
* 3. flex 안에 여러 독립 토글
*
* mw-customtoggle-TWICE_KOR
* mw-customtoggle-TWICE_OVS
* mw-customtoggle-TWICE_ETC
* mw-customtoggle-TWICE_REL
*
*
* 애니메이션:
* 높이만 변경
* opacity 사용 안 함
  */
  */


28번째 줄: 60번째 줄:
'use strict';
'use strict';


/**
* 애니메이션 시간
*/
var DURATION = 300;
var DURATION = 300;
/**
* jQuery easing
*/
var EASING = 'swing';
var EASING = 'swing';


/**
/**
* 토글 이름 추출
* jQuery data key
*/
var DATA_DISPLAY =
'mwCustomCollapseDisplay';
 
 
/**
* -----------------------------------------------------
* 토글 클래스에서 식별자 추출
*
*
* mw-customtoggle-Flora_LACTEA_01
* mw-customtoggle-Flora_LACTEA_01
* →
* →
* Flora_LACTEA_01
* Flora_LACTEA_01
* -----------------------------------------------------
*/
*/
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 (
i = 0;
i < classes.length;
i++
) {
 
if (
if (
classes[ i ].indexOf( prefix ) === 0
classes[ i ].indexOf(
prefix
) === 0
) {
) {
return classes[ i ].substring(
return classes[ i ].substring(
prefix.length
prefix.length
52번째 줄: 113번째 줄:
}
}
}
}


return null;
return null;
58번째 줄: 120번째 줄:


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




/**
/**
* 동일한 이름을 사용하는
* -----------------------------------------------------
* 모든 collapsible 대상 가져오기
* 같은 name에 대응하는 모든 collapsible 가져오기
*
* 동일 ID가 여러 개 있어도 모두 반환
* -----------------------------------------------------
*/
*/
function getTargets( name ) {
function getTargets( name ) {
if ( !name ) {
return [];
}
var id =
var id =
'mw-customcollapsible-' + name;
'mw-customcollapsible-' +
name;
 


return document.querySelectorAll(
return document.querySelectorAll(
84번째 줄: 166번째 줄:


/**
/**
* 직계 .mw-collapsible-content 찾기
* -----------------------------------------------------
* 해당 target의 직계
* .mw-collapsible-content 찾기
*
*
* 중첩 collapsible까지 잘못 선택하지 않도록
* querySelector를 바로 사용하지 않는 이유:
* children만 검사합니다.
* 안쪽에 중첩된 collapsible이 있을 경우
* 내부 콘텐츠를 잘못 잡을 수 있음.
* -----------------------------------------------------
*/
*/
function getDirectContent( target ) {
function getDirectContent( target ) {
var children = target.children;
 
var children =
target.children;
 
var i;
var i;


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


return null;
return null;
108번째 줄: 207번째 줄:


/**
/**
* 실제 애니메이션 대상
* -----------------------------------------------------
* 실제 애니메이션할 요소 결정
*
*
* MediaWiki 표준:
* 일반 MediaWiki:
* .mw-collapsible-content
* .mw-collapsible-content
*
*
* 현재 LACTEA 구조:
* LACTEA 등의 구조:
* target 자체
* target 자체
* -----------------------------------------------------
*/
*/
function getAnimatedElement( target ) {
function getAnimatedElement( target ) {
var content =
var content =
getDirectContent( target );
getDirectContent(
target
);
 


if ( content ) {
if ( content ) {
return content;
return content;
}
}


return target;
return target;
129번째 줄: 235번째 줄:


/**
/**
* 현재 접혀 있는지 확인
* -----------------------------------------------------
* 현재 접힌 상태인지 확인
* -----------------------------------------------------
*/
*/
function isCollapsed( target, element ) {
function isCollapsed(
target,
element
) {


/*
* MediaWiki 상태 클래스 우선
*/
if (
if (
target.classList.contains(
target.classList.contains(
138번째 줄: 252번째 줄:
)
)
) {
) {
return true;
return true;
}
}


/*
* 실제 display:none 여부 확인
*/
return (
return (
window.getComputedStyle(
window
element
.getComputedStyle(
).display === 'none'
element
)
.display === 'none'
);
);
}
}
150번째 줄: 271번째 줄:


/**
/**
* 펼칠 때 사용할 display 계산
* -----------------------------------------------------
* 현재 display 값 저장
*
* inline-block 같은 display를 접었다 다시 펼칠 때
* 원래 형태를 복구하기 위해 사용
* -----------------------------------------------------
*/
function rememberDisplay( element ) {
 
var display =
window
.getComputedStyle(
element
)
.display;
 
 
if (
display &&
display !== 'none'
) {
 
$( element ).data(
DATA_DISPLAY,
display
);
}
}
 
 
/**
* -----------------------------------------------------
* 펼칠 때 사용할 display 결정
* -----------------------------------------------------
*/
*/
function getOpenDisplay( element ) {
function getOpenDisplay( element ) {


/*
/*
* 이전에 저장된 display가 있으면 사용
* 이전에 저장된 display가 있다면 사용
*/
*/
var stored =
var stored =
$( element ).data(
$( element ).data(
'mwAnimatedDisplay'
DATA_DISPLAY
);
);


if ( stored ) {
if ( stored ) {
return stored;
return stored;
}
}
var display =
window.getComputedStyle(
element
).display;




/*
/*
* 현재 보이는 상태라면 저장
* 인라인 스타일에 유효한 display가 남아 있는 경우
*/
*/
if (
if (
display &&
element.style.display &&
display !== 'none'
element.style.display !== 'none'
) {
) {


$( element ).data(
return element.style.display;
'mwAnimatedDisplay',
display
);
 
return display;
}
}




/*
/*
* mw-collapsible 자체를 펼치는 구조
* 현재 computed display
*/
*/
var computed =
window
.getComputedStyle(
element
)
.display;
if (
if (
element.classList.contains(
computed &&
'mw-collapsible'
computed !== 'none'
)
) {
) {


/*
return computed;
* width가 100%인 내용 영역은 block이
* 가장 자연스럽습니다.
*/
if (
element.style.width === '100%'
) {
return 'block';
}
 
return 'inline-block';
}
}




/*
* 요소별 자연스러운 display
*/
switch ( element.tagName ) {
switch ( element.tagName ) {


case 'TABLE':
case 'TABLE':
return 'table';
return 'table';
case 'TBODY':
return 'table-row-group';
case 'THEAD':
return 'table-header-group';
case 'TFOOT':
return 'table-footer-group';


case 'TR':
case 'TR':
224번째 줄: 377번째 줄:
case 'TH':
case 'TH':
return 'table-cell';
return 'table-cell';
case 'COLGROUP':
return 'table-column-group';
case 'COL':
return 'table-column';
case 'CAPTION':
return 'table-caption';


case 'LI':
case 'LI':
return 'list-item';
return 'list-item';
}
/*
* mw-collapsible 자체가 대상인 경우
*
* 폭이 100%면 block,
* 그렇지 않으면 inline-block을 우선 사용.
*
* LACTEA 상단 버튼 영역 같은
* display:inline-block 구조 대응.
*/
if (
element.classList.contains(
'mw-collapsible'
)
) {
if (
element.style.width === '100%' ||
element.style.width === '100.0%'
) {


default:
return 'block';
return 'block';
}
}
}




/**
return 'inline-block';
* 현재 display 값을 기억
}
*
* 닫기 전에 호출해야 함
*/
function rememberDisplay( element ) {


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


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




/**
/**
* 같은 name의 모든 토글에
* -----------------------------------------------------
* aria-expanded 설정
* 같은 name의 모든 토글 ARIA 상태 변경
* -----------------------------------------------------
*/
*/
function updateToggleAria(
function updateAria(
name,
name,
expanded
expanded
268번째 줄: 435번째 줄:


var className =
var className =
'mw-customtoggle-' + name;
'mw-customtoggle-' +
name;


var toggles =
var toggles =
277번째 줄: 445번째 줄:
var i;
var i;


for ( i = 0; i < toggles.length; i++ ) {
 
for (
i = 0;
i < toggles.length;
i++
) {
 
toggles[ i ].setAttribute(
toggles[ i ].setAttribute(
'aria-expanded',
'aria-expanded',
289번째 줄: 463번째 줄:


/**
/**
* -----------------------------------------------------
* 펼치기
* 펼치기
* -----------------------------------------------------
*/
*/
function expand(
function expand(
300번째 줄: 476번째 줄:




/*
* 현재 진행 중인 jQuery animation 중지
*
* true  = queue 비움
* false = 현재 애니메이션을 끝까지 강제 적용하지 않음
*/
$element.stop(
$element.stop(
true,
true,
306번째 줄: 488번째 줄:




/*
* MediaWiki 접힘 상태 해제
*/
target.classList.remove(
target.classList.remove(
'mw-collapsed'
'mw-collapsed'
311번째 줄: 496번째 줄:




/*
/**
* 표준 MediaWiki 방식
* -------------------------------------------------
* 일반 MediaWiki 구조
*
*
* target 안의
* target
* .mw-collapsible-content만 처리
* └─ .mw-collapsible-content
* -------------------------------------------------
*/
*/
if ( element !== target ) {
if (
element !== target
) {


/*
* slideDown이 display:none에서
* 자연스럽게 높이를 계산
*/
$element
$element
.stop( true, false )
.stop(
true,
false
)
.slideDown(
.slideDown(
DURATION
{
duration:
DURATION,
 
easing:
EASING,
 
complete:
function () {
 
/*
* jQuery가 생성한
* 불필요한 height/overflow 제거
*/
$element.css(
{
height:
'',
 
overflow:
''
}
);
}
}
);
);


return;
return;
329번째 줄: 550번째 줄:




/*
/**
* LACTEA 같은 구조
* -------------------------------------------------
* target 자체가 display:none 되는 구조
*
*
* mw-collapsible 자체가 display:none이므로
* LACTEA 등의 구조
* 직접 높이를 애니메이션
* -------------------------------------------------
*/
*/


342번째 줄: 564번째 줄:




/*
* 먼저 보이는 상태로 복구
*/
$element.css(
$element.css(
{
{
display: display,
display:
height: 'auto',
display,
overflow: 'hidden'
 
height:
'auto',
 
overflow:
'hidden'
}
}
);
);




/*
* 실제 최종 높이 측정
*/
var targetHeight =
var targetHeight =
element.scrollHeight;
element.scrollHeight;




/*
* 0 높이에서 시작
*/
$element.css(
$element.css(
{
{
height: 0,
height:
opacity: 0
'0px'
}
}
);
);
364번째 줄: 600번째 줄:


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




/*
* 높이만 애니메이션
*/
$element.animate(
$element.animate(
{
{
height:
height:
targetHeight,
targetHeight
 
opacity:
1
},
},
{
{
387번째 줄: 623번째 줄:
function () {
function () {


/*
* 최종적으로 내용 높이에
* 자동 대응하도록 height 제거
*/
$element.css(
$element.css(
{
{
height: '',
height:
overflow: '',
'',
opacity: ''
 
overflow:
''
}
}
);
);
401번째 줄: 643번째 줄:


/**
/**
* -----------------------------------------------------
* 접기
* 접기
* -----------------------------------------------------
*/
*/
function collapse(
function collapse(
419번째 줄: 663번째 줄:


/*
/*
* 현재 display를 반드시 먼저 기억
* display:none으로 만들기 전에
* 현재 display 기억
*/
*/
rememberDisplay(
rememberDisplay(
426번째 줄: 671번째 줄:




/*
/**
* 표준 MediaWiki 구조
* -------------------------------------------------
* 일반 MediaWiki 구조
* -------------------------------------------------
*/
*/
if ( element !== target ) {
if (
element !== target
) {


$element.slideUp(
$element.slideUp(
DURATION,
{
function () {
duration:
DURATION,


target.classList.add(
easing:
'mw-collapsed'
EASING,
);
 
complete:
function () {
 
target.classList.add(
'mw-collapsed'
);
 
 
$element.css(
{
height:
'',
 
overflow:
''
}
);
}
}
}
);
);


return;
return;
445번째 줄: 714번째 줄:




/*
/**
* mw-collapsible 자체가 접히는 구조
* -------------------------------------------------
* target 자체가 접히는 구조
* -------------------------------------------------
*/
*/


455번째 줄: 726번째 줄:




/*
* 시작 높이를 고정
*/
$element.css(
$element.css(
{
{
height:
height:
startHeight,
startHeight + 'px',


overflow:
overflow:
'hidden',
'hidden'
 
opacity:
1
}
}
);
);




/*
* reflow
*/
void element.offsetHeight;
void element.offsetHeight;




/*
* 현재 높이 → 0
*/
$element.animate(
$element.animate(
{
{
height: 0,
height:
opacity: 0
0
},
},
{
{
490번째 줄: 761번째 줄:
function () {
function () {


/*
* 완전히 접힌 뒤 상태 반영
*/
target.classList.add(
target.classList.add(
'mw-collapsed'
'mw-collapsed'
504번째 줄: 778번째 줄:


overflow:
overflow:
'',
opacity:
''
''
}
}
517번째 줄: 788번째 줄:


/**
/**
* 하나의 name에 연결된 대상 전체 토글
* -----------------------------------------------------
* 대상 하나 토글
* -----------------------------------------------------
*/
function toggleTarget(
target
) {
 
var element =
getAnimatedElement(
target
);
 
 
if (
isCollapsed(
target,
element
)
) {
 
expand(
target,
element
);
 
return true;
}
 
 
collapse(
target,
element
);
 
 
return false;
}
 
 
/**
* -----------------------------------------------------
* 그룹 토글
*
* 동일한 XXX를 가진 target이 여러 개면
* 각각 현재 상태의 반대로 변경.
*
* 예:
*
* LACTEA
*
* 첫 번째:
* 버튼만 표시
*
* 두 번째:
* 실제 내용, display:none
*
* 클릭하면
*
* 첫 번째 → 닫힘
* 두 번째 → 펼침
*
* 따라서 버튼이 중복 표시되지 않음.
* -----------------------------------------------------
*/
*/
function toggleGroup( toggle ) {
function toggleGroup(
toggle
) {


var name =
var name =
525번째 줄: 861번째 줄:
toggle
toggle
);
);


if ( !name ) {
if ( !name ) {
536번째 줄: 873번째 줄:
);
);


if ( !targets.length ) {
 
if (
!targets ||
!targets.length
) {
 
return false;
return false;
}
}
543번째 줄: 885번째 줄:
var i;
var i;


/*
var openedAny =
* 핵심:
false;
*
 
* 하나만 선택하지 않고
 
* 같은 이름을 가진 모든 target을
* 각자 반대 상태로 변경
*/
for (
for (
i = 0;
i = 0;
564번째 줄: 903번째 줄:
);
);


 
var collapsed =
if (
isCollapsed(
isCollapsed(
target,
target,
element
element
)
);
) {
 
 
if ( collapsed ) {


expand(
expand(
576번째 줄: 916번째 줄:
element
element
);
);
openedAny =
true;


} else {
} else {
588번째 줄: 931번째 줄:


/*
/*
* 이 형태에서는 보이는 토글과 숨겨진 토글이
* 대상이 하나뿐인 일반 구조에서는
* 교대하므로 aria-expanded를 특정 하나에
* ARIA를 정상적으로 갱신.
* 일괄 적용하는 것은 생략합니다.
*
* 동일 ID가 2개인 교대형 구조는
* 두 요소의 상태가 서로 반대이므로
* 클릭된 그룹 기준으로 처리.
*/
*/
if (
targets.length === 1
) {
updateAria(
name,
openedAny
);
}




599번째 줄: 954번째 줄:


/**
/**
* 클릭
* -----------------------------------------------------
* 클릭 처리
*
*
* MediaWiki 기본 customtoggle보다
* capture=true로
* 먼저 실행
* MediaWiki jquery.makeCollapsible보다
* 먼저 이벤트를 처리.
* -----------------------------------------------------
*/
*/
document.addEventListener(
document.addEventListener(
612번째 줄: 970번째 줄:
'.mw-customtoggle'
'.mw-customtoggle'
);
);


if ( !toggle ) {
if ( !toggle ) {
618번째 줄: 977번째 줄:




/*
* 우리가 처리하는 customtoggle인지 확인
*/
if (
if (
!getToggleName(
!getToggleName(
623번째 줄: 985번째 줄:
)
)
) {
) {
return;
return;
}
}




/*
* MediaWiki 기본 토글 동작 중지
*/
event.preventDefault();
event.preventDefault();


643번째 줄: 1,009번째 줄:


/**
/**
* -----------------------------------------------------
* 키보드
*
* Enter / Space
* Enter / Space
* -----------------------------------------------------
*/
*/
document.addEventListener(
document.addEventListener(
653번째 줄: 1,023번째 줄:
event.key !== ' '
event.key !== ' '
) {
) {
return;
return;
}
}
661번째 줄: 1,032번째 줄:
'.mw-customtoggle'
'.mw-customtoggle'
);
);


if ( !toggle ) {
if ( !toggle ) {
672번째 줄: 1,044번째 줄:
)
)
) {
) {
return;
return;
}
}
689번째 줄: 1,062번째 줄:
true
true
);
);
/**
* -----------------------------------------------------
* 초기 display 상태 기억
*
* 페이지 처음 로딩됐을 때 현재 보이는
* mw-collapsible의 display 값을 저장.
*
* 예:
* display:inline-block
*
* 나중에 접었다 펼쳐도
* inline-block으로 정확히 복원.
* -----------------------------------------------------
*/
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
);
}
}
}
/**
* -----------------------------------------------------
* 최초 로딩
* -----------------------------------------------------
*/
$( function () {
initializeDisplays(
document
);
} );
/**
* -----------------------------------------------------
* MediaWiki 동적 콘텐츠 대응
*
* Lua / AJAX / 확장기능 등으로
* 나중에 추가된 콘텐츠의 display도 저장.
* -----------------------------------------------------
*/
if (
mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function ( $content ) {
if (
$content &&
$content[ 0 ]
) {
initializeDisplays(
$content[ 0 ]
);
}
}
);
}


}( jQuery, mediaWiki ) );
}( jQuery, mediaWiki ) );

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

/**
 * MediaWiki 커스텀 펼치기 / 접기 애니메이션
 *
 * 지원 구조
 *
 * 1. 일반 MediaWiki 구조
 *
 * <div class="mw-customtoggle-XXX mw-customtoggle">
 *     [ 펼치기 · 접기 ]
 * </div>
 *
 * <div
 *     id="mw-customcollapsible-XXX"
 *     class="mw-collapsible mw-collapsed"
 * >
 *     <div class="mw-collapsible-content">
 *         내용
 *     </div>
 * </div>
 *
 *
 * 2. 동일 ID를 2개 사용하는 전환형 구조
 *
 * <div
 *     id="mw-customcollapsible-XXX"
 *     class="mw-collapsible"
 * >
 *     <div class="mw-customtoggle-XXX mw-customtoggle">
 *         [ 버튼 ]
 *     </div>
 * </div>
 *
 * <div
 *     id="mw-customcollapsible-XXX"
 *     class="mw-collapsible mw-collapsed"
 *     style="display:none;"
 * >
 *     <div class="mw-customtoggle-XXX mw-customtoggle">
 *         [ 버튼 ]
 *     </div>
 *
 *     실제 내용
 * </div>
 *
 *
 * 3. flex 안에 여러 독립 토글
 *
 * mw-customtoggle-TWICE_KOR
 * mw-customtoggle-TWICE_OVS
 * mw-customtoggle-TWICE_ETC
 * mw-customtoggle-TWICE_REL
 *
 *
 * 애니메이션:
 * 높이만 변경
 * opacity 사용 안 함
 */

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

	/**
	 * 애니메이션 시간
	 */
	var DURATION = 300;

	/**
	 * jQuery easing
	 */
	var EASING = 'swing';

	/**
	 * jQuery data key
	 */
	var DATA_DISPLAY =
		'mwCustomCollapseDisplay';


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


	/**
	 * -----------------------------------------------------
	 * HTML attribute selector escape
	 * -----------------------------------------------------
	 */
	function escapeAttribute( value ) {

		return String( value )
			.replace(
				/\\/g,
				'\\\\'
			)
			.replace(
				/"/g,
				'\\"'
			);
	}


	/**
	 * -----------------------------------------------------
	 * 같은 name에 대응하는 모든 collapsible 가져오기
	 *
	 * 동일 ID가 여러 개 있어도 모두 반환
	 * -----------------------------------------------------
	 */
	function getTargets( name ) {

		if ( !name ) {
			return [];
		}


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


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


	/**
	 * -----------------------------------------------------
	 * 해당 target의 직계
	 * .mw-collapsible-content 찾기
	 *
	 * querySelector를 바로 사용하지 않는 이유:
	 * 안쪽에 중첩된 collapsible이 있을 경우
	 * 내부 콘텐츠를 잘못 잡을 수 있음.
	 * -----------------------------------------------------
	 */
	function getDirectContent( target ) {

		var children =
			target.children;

		var i;


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

			if (
				children[ i ]
					.classList
					.contains(
						'mw-collapsible-content'
					)
			) {

				return children[ i ];
			}
		}


		return null;
	}


	/**
	 * -----------------------------------------------------
	 * 실제 애니메이션할 요소 결정
	 *
	 * 일반 MediaWiki:
	 * .mw-collapsible-content
	 *
	 * LACTEA 등의 구조:
	 * target 자체
	 * -----------------------------------------------------
	 */
	function getAnimatedElement( target ) {

		var content =
			getDirectContent(
				target
			);


		if ( content ) {
			return content;
		}


		return target;
	}


	/**
	 * -----------------------------------------------------
	 * 현재 접힌 상태인지 확인
	 * -----------------------------------------------------
	 */
	function isCollapsed(
		target,
		element
	) {

		/*
		 * MediaWiki 상태 클래스 우선
		 */
		if (
			target.classList.contains(
				'mw-collapsed'
			)
		) {

			return true;
		}


		/*
		 * 실제 display:none 여부 확인
		 */
		return (
			window
				.getComputedStyle(
					element
				)
				.display === 'none'
		);
	}


	/**
	 * -----------------------------------------------------
	 * 현재 display 값 저장
	 *
	 * inline-block 같은 display를 접었다 다시 펼칠 때
	 * 원래 형태를 복구하기 위해 사용
	 * -----------------------------------------------------
	 */
	function rememberDisplay( element ) {

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


		if (
			display &&
			display !== 'none'
		) {

			$( element ).data(
				DATA_DISPLAY,
				display
			);
		}
	}


	/**
	 * -----------------------------------------------------
	 * 펼칠 때 사용할 display 결정
	 * -----------------------------------------------------
	 */
	function getOpenDisplay( element ) {

		/*
		 * 이전에 저장된 display가 있다면 사용
		 */
		var stored =
			$( element ).data(
				DATA_DISPLAY
			);


		if ( stored ) {
			return stored;
		}


		/*
		 * 인라인 스타일에 유효한 display가 남아 있는 경우
		 */
		if (
			element.style.display &&
			element.style.display !== 'none'
		) {

			return element.style.display;
		}


		/*
		 * 현재 computed display
		 */
		var computed =
			window
				.getComputedStyle(
					element
				)
				.display;


		if (
			computed &&
			computed !== 'none'
		) {

			return computed;
		}


		/*
		 * 요소별 자연스러운 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 'COLGROUP':
				return 'table-column-group';

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

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

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


		/*
		 * mw-collapsible 자체가 대상인 경우
		 *
		 * 폭이 100%면 block,
		 * 그렇지 않으면 inline-block을 우선 사용.
		 *
		 * LACTEA 상단 버튼 영역 같은
		 * display:inline-block 구조 대응.
		 */
		if (
			element.classList.contains(
				'mw-collapsible'
			)
		) {

			if (
				element.style.width === '100%' ||
				element.style.width === '100.0%'
			) {

				return 'block';
			}


			return 'inline-block';
		}


		return 'block';
	}


	/**
	 * -----------------------------------------------------
	 * 같은 name의 모든 토글 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
	) {

		var $element =
			$( element );


		/*
		 * 현재 진행 중인 jQuery animation 중지
		 *
		 * true  = queue 비움
		 * false = 현재 애니메이션을 끝까지 강제 적용하지 않음
		 */
		$element.stop(
			true,
			false
		);


		/*
		 * MediaWiki 접힘 상태 해제
		 */
		target.classList.remove(
			'mw-collapsed'
		);


		/**
		 * -------------------------------------------------
		 * 일반 MediaWiki 구조
		 *
		 * target
		 * └─ .mw-collapsible-content
		 * -------------------------------------------------
		 */
		if (
			element !== target
		) {

			/*
			 * slideDown이 display:none에서
			 * 자연스럽게 높이를 계산
			 */
			$element
				.stop(
					true,
					false
				)
				.slideDown(
					{
						duration:
							DURATION,

						easing:
							EASING,

						complete:
							function () {

								/*
								 * jQuery가 생성한
								 * 불필요한 height/overflow 제거
								 */
								$element.css(
									{
										height:
											'',

										overflow:
											''
									}
								);
							}
					}
				);


			return;
		}


		/**
		 * -------------------------------------------------
		 * target 자체가 display:none 되는 구조
		 *
		 * LACTEA 등의 구조
		 * -------------------------------------------------
		 */

		var display =
			getOpenDisplay(
				element
			);


		/*
		 * 먼저 보이는 상태로 복구
		 */
		$element.css(
			{
				display:
					display,

				height:
					'auto',

				overflow:
					'hidden'
			}
		);


		/*
		 * 실제 최종 높이 측정
		 */
		var targetHeight =
			element.scrollHeight;


		/*
		 * 0 높이에서 시작
		 */
		$element.css(
			{
				height:
					'0px'
			}
		);


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


		/*
		 * 높이만 애니메이션
		 */
		$element.animate(
			{
				height:
					targetHeight
			},
			{
				duration:
					DURATION,

				easing:
					EASING,

				complete:
					function () {

						/*
						 * 최종적으로 내용 높이에
						 * 자동 대응하도록 height 제거
						 */
						$element.css(
							{
								height:
									'',

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


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

		var $element =
			$( element );


		$element.stop(
			true,
			false
		);


		/*
		 * display:none으로 만들기 전에
		 * 현재 display 기억
		 */
		rememberDisplay(
			element
		);


		/**
		 * -------------------------------------------------
		 * 일반 MediaWiki 구조
		 * -------------------------------------------------
		 */
		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
				.getBoundingClientRect()
				.height;


		/*
		 * 시작 높이를 고정
		 */
		$element.css(
			{
				height:
					startHeight + 'px',

				overflow:
					'hidden'
			}
		);


		void element.offsetHeight;


		/*
		 * 현재 높이 → 0
		 */
		$element.animate(
			{
				height:
					0
			},
			{
				duration:
					DURATION,

				easing:
					EASING,

				complete:
					function () {

						/*
						 * 완전히 접힌 뒤 상태 반영
						 */
						target.classList.add(
							'mw-collapsed'
						);


						$element.css(
							{
								display:
									'none',

								height:
									'',

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


	/**
	 * -----------------------------------------------------
	 * 대상 하나 토글
	 * -----------------------------------------------------
	 */
	function toggleTarget(
		target
	) {

		var element =
			getAnimatedElement(
				target
			);


		if (
			isCollapsed(
				target,
				element
			)
		) {

			expand(
				target,
				element
			);

			return true;
		}


		collapse(
			target,
			element
		);


		return false;
	}


	/**
	 * -----------------------------------------------------
	 * 그룹 토글
	 *
	 * 동일한 XXX를 가진 target이 여러 개면
	 * 각각 현재 상태의 반대로 변경.
	 *
	 * 예:
	 *
	 * LACTEA
	 *
	 * 첫 번째:
	 * 버튼만 표시
	 *
	 * 두 번째:
	 * 실제 내용, display:none
	 *
	 * 클릭하면
	 *
	 * 첫 번째 → 닫힘
	 * 두 번째 → 펼침
	 *
	 * 따라서 버튼이 중복 표시되지 않음.
	 * -----------------------------------------------------
	 */
	function toggleGroup(
		toggle
	) {

		var name =
			getToggleName(
				toggle
			);


		if ( !name ) {
			return false;
		}


		var targets =
			getTargets(
				name
			);


		if (
			!targets ||
			!targets.length
		) {

			return false;
		}


		var i;

		var openedAny =
			false;


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

				openedAny =
					true;

			} else {

				collapse(
					target,
					element
				);
			}
		}


		/*
		 * 대상이 하나뿐인 일반 구조에서는
		 * ARIA를 정상적으로 갱신.
		 *
		 * 동일 ID가 2개인 교대형 구조는
		 * 두 요소의 상태가 서로 반대이므로
		 * 클릭된 그룹 기준으로 처리.
		 */
		if (
			targets.length === 1
		) {

			updateAria(
				name,
				openedAny
			);
		}


		return true;
	}


	/**
	 * -----------------------------------------------------
	 * 클릭 처리
	 *
	 * capture=true로
	 * MediaWiki jquery.makeCollapsible보다
	 * 먼저 이벤트를 처리.
	 * -----------------------------------------------------
	 */
	document.addEventListener(
		'click',
		function ( event ) {

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


			if ( !toggle ) {
				return;
			}


			/*
			 * 우리가 처리하는 customtoggle인지 확인
			 */
			if (
				!getToggleName(
					toggle
				)
			) {

				return;
			}


			/*
			 * MediaWiki 기본 토글 동작 중지
			 */
			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 상태 기억
	 *
	 * 페이지 처음 로딩됐을 때 현재 보이는
	 * mw-collapsible의 display 값을 저장.
	 *
	 * 예:
	 * display:inline-block
	 *
	 * 나중에 접었다 펼쳐도
	 * inline-block으로 정확히 복원.
	 * -----------------------------------------------------
	 */
	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
				);
			}
		}
	}


	/**
	 * -----------------------------------------------------
	 * 최초 로딩
	 * -----------------------------------------------------
	 */
	$( function () {

		initializeDisplays(
			document
		);

	} );


	/**
	 * -----------------------------------------------------
	 * MediaWiki 동적 콘텐츠 대응
	 *
	 * Lua / AJAX / 확장기능 등으로
	 * 나중에 추가된 콘텐츠의 display도 저장.
	 * -----------------------------------------------------
	 */
	if (
		mw &&
		mw.hook
	) {

		mw.hook(
			'wikipage.content'
		).add(
			function ( $content ) {

				if (
					$content &&
					$content[ 0 ]
				) {

					initializeDisplays(
						$content[ 0 ]
					);
				}
			}
		);
	}


}( jQuery, mediaWiki ) );